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) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 = ''; 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 = `${esc(text)}`; } 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}) => ``).join(''); const list=$('examples'),chip=list.querySelector('.selected'); if(chip&&(chip.offsetLeftlist.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)=>`
`).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 `

Returns the probability of yes, from 0 to 1.

Define what yes and no mean${['true','false'].map(k=>``).join('')}
`; const entries = q.type === 'choice' ? Object.entries(q.criteria) : q.criteria.map((text,i) => [String(i),text]); return `
${entries.map(([name,text],i)=>`
${q.type==='score'?i:String.fromCharCode(65+i%26)}${q.type==='choice'?``:''}
`).join('')}
${q.type==='score'?'Ordered from lowest to highest':'Names and descriptions reach the model'}
`; } function renderQuestions() { $('question-count').textContent = draft.questions.length; updateRunButton(); $('questions').innerHTML = draft.questions.map((q,i)=>`
${choicesHTML(q,i)}
`).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 ? `${esc(a.choice)}${pct(a.probabilities[a.choice])}` : `—${ids.length} options`; const rows = order.map((id, i) => { const p = a ? a.probabilities[id] : 0, top = a && id === a.choice, description = display(q.criteria[id]); return `
${esc(id)}${a ? pct(p) : '—'}
`; }).join(''); return `
${head}
${rows}
`; } function noulBody(q, a) { const p = a ? a.noul : 0, yes = a && a.noul >= 0.5; const head = a ? `${yes ? 'Yes' : 'No'}P(yes) ${pct(p)}` : `—Yes or no`; return `
${head}
`; } 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 ? `${a.score.toFixed(2)} / ${n - 1}${esc(shortLevel(levels[top]))} · ${pct(probs[top])}` : `—${n} levels`; const cols = levels.map((level, i) => `
${a ? pct(probs[i]) : '—'}${i} ${esc(shortLevel(level))}
`).join(''); const marker = a ? `` : ''; return `
${head}
${cols}${marker}
`; } 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 `
${esc(id)}${TYPE_LABEL[q.type]}

${esc(display(q.instructions))}

${body}
`; } 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 ? `

${lede}

` : ''}
${cards}
`; } function compactDecision(a,q){ if(a.type==='noul'){const yes=a.noul>=.5;return `${yes?'Yes':'No'}P(yes) ${pct(a.noul)}`;} if(a.type==='score'){const best=Object.entries(a.probabilities).reduce((x,y)=>y[1]>x[1]?y:x);return `${a.score.toFixed(2)} / ${q.criteria.length-1}Level ${esc(best[0])} · ${pct(best[1])}`;} return `${esc(a.choice)}${pct(a.probabilities[a.choice])}`; } 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=`

Shared questions, separate decisions. Open a row for every probability.

${qids.map(id=>``).join('')}${rows.map((r,i)=>`${qids.map(id=>``).join('')}`).join('')}
Context${esc(id)}${TYPE_LABEL[payload.questions[id].type]}
${compactDecision(r.answers[id],payload.questions[id])}
`; }else{ $('results').innerHTML=`
${qids.map((id,i)=>decisionCard(id,payload.questions[id],result.answers[id],i)).join('')}
`; } 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=``;}} 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 ${esc(generation)}`; 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 ``; }).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 ? `${esc(sp.name)}${sp.generation ? ` ${esc(sp.generation)}` : ''}${esc(facts.join(' · '))}` : ''; $('lineup').hidden = false; } function renderModelTable() { const body = $('model-table').tBodies[0]; body.innerHTML = groupModels(models).map(group => `${esc(group.label)}${group.models.length} model${group.models.length === 1 ? '' : 's'}` + 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 `${esc(p.codename)}${p.reasoning ? 'Reasoning' : ''}${p.description ? `${esc(p.description)}` : ''}${esc(p.size || '—')}${tokenText(m.complete_input_tokens)} tokens${esc(p.releaseLabel || '—')}${r.label}Hub `; }).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.clientXr.right||e.clientYr.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();