Spaces:
Paused
Paused
Download static/app.js from vllm-sr/decision1-studio: direct link, hf CLI and curl.
- Browser
- Download file 44.9 kB
-
https://huggingface.co/spaces/vllm-sr/decision1-studio/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/vllm-sr/decision1-studio/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/vllm-sr/decision1-studio/resolve/main/static/app.js
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) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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(); | |