Spaces:
Sleeping
Sleeping
Download static/app.js from JetBrains-Research/layer-lab: direct link, hf CLI and curl.
- Browser
- Download file 47.9 kB
-
https://huggingface.co/spaces/JetBrains-Research/layer-lab/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/JetBrains-Research/layer-lab/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/JetBrains-Research/layer-lab/resolve/main/static/app.js
47.9 kB
| const $ = (selector) => document.querySelector(selector); | |
| const $$ = (selector) => [...document.querySelectorAll(selector)]; | |
| const escape = (text) => String(text).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); | |
| const visible = (text) => String(text).replace(/ /g, '·').replace(/\n/g, '↵').replace(/\t/g, '⇥').replace(/\r/g, '␍') || '∅'; | |
| const percent = (p) => `${(p * 100).toFixed(p >= .1 ? 1 : p >= .001 ? 2 : 3)}%`; | |
| const colorRGB = (value) => { | |
| const p = Math.max(0, Math.min(1, value)); | |
| return [Math.round(245-238*p),Math.round(251-160*p),Math.round(247-195*p)]; | |
| }; | |
| const color = value => `rgb(${colorRGB(value).join(',')})`; | |
| const ink = value => { | |
| const linear=colorRGB(value).map(v=>{const s=v/255;return s<=.04045?s/12.92:((s+.055)/1.055)**2.4;}); | |
| return .2126*linear[0]+.7152*linear[1]+.0722*linear[2]>.179?'#000000':'#ffffff'; | |
| }; | |
| const QWEN_ID = 'Qwen/Qwen3-0.6B'; | |
| const state = { | |
| model:QWEN_ID, | |
| mode:'chat', messages:[], thinking:false, raw:'', revision:0, prepared:null, | |
| run:null, runRevision:null, layer:27, head:0, query:0, key:0, lens:'token', busy:false, | |
| appendPreview:null, appendVersion:0, | |
| status:null, selection:null, matrix:null, matrixCache:new Map(), selectionVersion:0, | |
| lensColor:'probability', lensHead:'mean', layerAttention:null, layerAttentionCache:new Map(), layerAttentionVersion:0, | |
| }; | |
| let prepareTimer, prepareVersion = 0, lastEditor = null, autoStarted = false, busyTimer, statusTimer, appendTimer; | |
| const descriptions = { | |
| '<|im_start|>':'Starts a message, followed by the role (system, user, or assistant) and a newline.', | |
| '<|im_end|>':'Ends a message; also marks the end of an assistant response.', | |
| '<|endoftext|>':'Text boundary. Chat messages normally use im_end.', | |
| '<think>':'Starts a reasoning block. Added token ID 151667, special=false.', | |
| '</think>':'Ends a reasoning block. Added token ID 151668, special=false.', | |
| '<tool_call>':'Starts a tool call in the Qwen format.', | |
| '</tool_call>':'Ends a tool call.', | |
| '<tool_response>':'Starts a tool response.', | |
| '</tool_response>':'Ends a tool response.', | |
| '<bos>':'Beginning of the Gemma input sequence.', | |
| '<eos>':'End-of-sequence marker.', | |
| '<start_of_turn>':'Starts a Gemma message, followed by user or model and a newline.', | |
| '<end_of_turn>':'Ends a Gemma turn; also stops this checkpoint’s response.', | |
| '<pad>':'Padding token. Normally excluded from the visible conversation.', | |
| '<unk>':'Unknown-token marker.', | |
| }; | |
| const primaryTokens = ['<|im_start|>','<|im_end|>','<|endoftext|>','<think>','</think>','<tool_call>','</tool_call>','<bos>','<eos>','<start_of_turn>','<end_of_turn>','<pad>','<unk>']; | |
| const supportsThinking = () => state.model === QWEN_ID; | |
| const kvHead = head => Math.floor(head / (state.run.heads / state.run.kv_heads)); | |
| function maskReason(query,key,layer=state.layer) { | |
| if(key>query)return 'Hidden by the causal mask'; | |
| if(state.run?.layer_types?.[layer]==='sliding_attention' && query-key>=state.run.sliding_window)return 'Outside the sliding attention window'; | |
| return ''; | |
| } | |
| function renderModel(meta) { | |
| $('#model-pill').textContent=meta.model===QWEN_ID?'Qwen3-0.6B':'Gemma3-270M'; | |
| $('#model-name').textContent=meta.name; | |
| $('#footer-model').textContent=`Model laboratory · ${meta.name}`; | |
| $('#checkpoint-link').href=`https://huggingface.co/${meta.model}`; | |
| $('#empty-model-note').textContent=`Computed from the actual ${meta.name} weights`; | |
| if(!meta.layers)return; | |
| const group=meta.heads/meta.kv_heads; | |
| $('#intro-description').innerHTML=`One context. ${meta.layers} layers. ${meta.layers*meta.heads} attention heads.<br>Trace how the prediction at each token changes.`; | |
| $('#model-shape').textContent=`${meta.layers} LAYERS / ${meta.heads} Q HEADS / ${meta.kv_heads} KV HEADS`; | |
| $('#model-description').textContent=`${meta.layers} layers · ${meta.heads} heads · ${meta.native_thinking?'native reasoning switch':'learned Reasoning / Solution format'}`; | |
| $('#diagram-layers').textContent=`${meta.layers} layers`; | |
| $('#metric-next-label').textContent=`Next token · layer ${meta.layers}`; | |
| $('#lens-footnote').textContent=`Input → L01 → L${String(meta.layers).padStart(2,'0')} ↑ · positions start at 0 · predictions refer to the next token`; | |
| $('#heads-heading').textContent=`All ${meta.heads} heads in this layer`; | |
| $('#attention-description').textContent=`Color shows how much attention the selected position assigns to each token: softmax(QKᵀ / √${meta.attention_scale} + attention mask). Choose one head or the arithmetic mean of all ${meta.heads} query heads after softmax.`; | |
| const windowNote=meta.sliding_window?` Sliding layers use a ${meta.sliding_window}-token window; layers ${meta.layer_types.map((type,i)=>type==='full_attention'?i+1:null).filter(Boolean).join(', ')} use full causal attention.`:''; | |
| $('#method-model').textContent=`${meta.name} has ${meta.layers} decoder layers, ${meta.heads} query heads, and ${meta.kv_heads} KV heads. With GQA, query head h uses KV head ⌊h / ${group}⌋.${windowNote}`; | |
| $('#method-lens').textContent=`After each layer, apply the final RMSNorm and shared language model head, then softmax over all ${meta.vocab_size.toLocaleString('en-US')} tokens. This is a logit lens: early layers were not trained to predict text independently. The final layer gives the standard model prediction.`; | |
| $('#method-reasoning').textContent=meta.native_thinking?'Chat uses the checkpoint’s apply_chat_template with enable_thinking. Disabling reasoning adds an empty <think>…</think> block. In Raw mode, you supply the control tokens and prefix. <think> is an added token with special=false.':'Chat uses the checkpoint’s standard Gemma template with <start_of_turn>user and <start_of_turn>model. This TinyGSM fine-tune generates ordinary Reasoning / Solution text. It has no special <think> token or native reasoning on/off switch. A system instruction is folded into the first user turn.'; | |
| $('#attention-formula').textContent=`A⁽ˡ,ʰ⁾ = softmax(QKᵀ / √${meta.attention_scale} + attention mask)`; | |
| if($('#layer-select').options.length!==meta.layers)$('#layer-select').innerHTML=Array.from({length:meta.layers},(_,i)=>`<option value="${i}">${i+1}</option>`).join(''); | |
| if($('#head-select').options.length!==meta.heads+1){ | |
| $('#head-select').innerHTML=`<option value="mean">Mean of all ${meta.heads} heads</option>`+Array.from({length:meta.heads},(_,i)=>`<option value="${i}">Head ${i}</option>`).join(''); | |
| $('#head-select').value=state.head; | |
| } | |
| if($('#lens-head').options.length!==meta.heads+1){ | |
| $('#lens-head').innerHTML=`<option value="mean">Mean of all ${meta.heads} heads</option>`+Array.from({length:meta.heads},(_,i)=>`<option value="${i}">Head ${i}</option>`).join(''); | |
| $('#lens-head').value=state.lensHead; | |
| } | |
| } | |
| async function switchModel(model) { | |
| if(state.busy||model===state.model)return; | |
| clearTimeout(statusTimer);clearTimeout(prepareTimer); | |
| state.model=model;state.status=null;state.run=null;state.prepared=null; | |
| closeAppendForm();state.runRevision=null; | |
| state.selection=null;state.matrix=null;state.matrixCache.clear();state.selectionVersion++;prepareVersion++; | |
| state.layerAttention=null;state.layerAttentionCache.clear();state.layerAttentionVersion++;state.lensHead='mean'; | |
| state.layer=0;state.head=0;state.query=0;state.key=0;autoStarted=false; | |
| if(!supportsThinking())state.thinking=false; | |
| $('#run-content').classList.add('hidden');$('#empty-state').classList.remove('hidden'); | |
| $('#stale-notice').classList.add('hidden');$('#export').disabled=true; | |
| for(const id of ['metric-tokens','metric-next','metric-time'])$(`#${id}`).textContent='—'; | |
| $('#metric-input').textContent='Exact model tokenization';$('#metric-prob').textContent='Probability over the full vocabulary'; | |
| $('#metric-runtime').textContent='Loading model…';$('#model-description').textContent='Loading model configuration…'; | |
| $('#prompt-preview').textContent='Loading tokenizer…';$('#token-count').textContent='— tokens'; | |
| $('#special-tokens').replaceChildren();$('#extra-tokens').replaceChildren(); | |
| $('#layer-select').replaceChildren();$('#head-select').replaceChildren(); | |
| error();renderEditor();updateRunButton();await pollStatus(); | |
| } | |
| async function api(path, data) { | |
| const response = await fetch(path, data === undefined ? {} : {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)}); | |
| if (!response.ok) { | |
| let detail; | |
| try { detail = (await response.json()).detail; } catch { detail = response.statusText; } | |
| throw new Error(typeof detail === 'string' ? detail : 'Check the context format and length.'); | |
| } | |
| return response.json(); | |
| } | |
| function payload() { | |
| return {model:state.model, mode:state.mode, messages:state.messages, text:state.raw, thinking:supportsThinking()&&state.thinking, max_new_tokens:0}; | |
| } | |
| function error(message='') { $('#input-error').textContent=message; $('#input-error').classList.toggle('hidden',!message); } | |
| function changed() { | |
| state.revision++; | |
| if (state.run) $('#stale-notice').classList.remove('hidden'); | |
| error(); | |
| clearTimeout(prepareTimer); | |
| // Invalidate an earlier response immediately, before the debounce starts. | |
| prepareVersion++; | |
| state.prepared=null; | |
| $('#token-count').textContent='Counting tokens…'; | |
| updateRunButton(); | |
| prepareTimer=setTimeout(prepare,300); | |
| } | |
| function updateRunButton() { | |
| $('#model-select').disabled=state.busy; | |
| $('#analyze').disabled=state.busy || state.status?.phase!=='ready' || !state.prepared?.fits; | |
| $('#analyze span:first-child').textContent=state.busy?'Computing…':'Inspect context'; | |
| updateAppendControls(); | |
| } | |
| function maybeAutoStart() { | |
| if(autoStarted||state.status?.phase!=='ready'||!state.prepared?.fits||state.busy||state.run)return; | |
| autoStarted=true; | |
| analyze(); | |
| } | |
| async function prepare() { | |
| if (!state.status?.tokenizer_ready) return; | |
| const version=++prepareVersion; | |
| try { | |
| const result=await api('/api/prepare',payload()); | |
| if(version!==prepareVersion)return; | |
| state.prepared=result; | |
| $('#prompt-preview').textContent=result.text; | |
| $('#token-count').textContent=`${result.count} / ${result.max_tokens} tokens`; | |
| if(!result.fits) error(`Context exceeds the ${result.max_tokens}-token limit. Input has not been truncated.`); | |
| } catch(exc) { | |
| if(version!==prepareVersion)return; | |
| state.prepared=null; | |
| $('#token-count').textContent='Could not prepare input'; | |
| $('#prompt-preview').textContent=exc.message; | |
| error(exc.message); | |
| } | |
| updateRunButton(); | |
| maybeAutoStart(); | |
| } | |
| function renderEditor() { | |
| $$('#messages textarea').forEach((el,i)=>{if(state.messages[i])state.messages[i].content=el.value;}); | |
| $('#chat-editor').classList.toggle('hidden',state.mode!=='chat'); | |
| $('#raw-editor').classList.toggle('hidden',state.mode!=='raw'); | |
| $('#raw-text').value=state.raw; | |
| $$('[data-mode]').forEach(button=>{const active=button.dataset.mode===state.mode;button.classList.toggle('active',active);button.setAttribute('aria-pressed',active);}); | |
| $('#format-label').textContent=state.mode==='chat'?'Official chat template':'Exact text, no wrapper'; | |
| $('#thinking').checked=state.thinking; | |
| $('#thinking').disabled=state.mode==='raw'||!supportsThinking(); | |
| $('#reasoning-switch').classList.toggle('hidden',!supportsThinking()); | |
| $('#thinking-note').textContent=!supportsThinking()?'Learned Reasoning / Solution text. No native on/off switch.':state.mode==='raw'?'Controlled by the Raw prefix':state.thinking?'The model can start a <think> block':'An empty <think> block precedes the answer'; | |
| $('#messages').innerHTML=state.messages.map((message,i)=>`<div class="message" data-index="${i}" data-role="${message.role}"><div class="message-top"><select aria-label="Message role ${i+1}" data-role-index="${i}">${['system','user','assistant'].map(role=>`<option ${message.role===role?'selected':''}>${role}</option>`).join('')}</select><button data-remove-message="${i}" aria-label="Remove message ${i+1}">×</button></div><textarea data-message-index="${i}" aria-label="Message ${i+1}, ${message.role}" rows="${message.content.length>100?4:3}" spellcheck="false">${escape(message.content)}</textarea></div>`).join(''); | |
| lastEditor=null; | |
| } | |
| function loadTemplate(name) { | |
| // Remove old editors before changing message state. | |
| $('#messages').innerHTML=''; | |
| state.mode='chat';state.thinking=false;state.raw=''; | |
| const templates={ | |
| question:[{role:'user',content:'What is the capital of France? Answer with one word.'}], | |
| math:[{role:'user',content:'Anna has 3 apples. She buys twice as many apples as she originally had. How many apples does Anna have now?'}], | |
| system:[{role:'system',content:'Answer in English using one word.'},{role:'user',content:'Which city is the capital of France?'}], | |
| dialogue:[{role:'user',content:'My name is Anna. I have a cat named Mars.'},{role:'assistant',content:'Nice to meet you, Anna!'},{role:'user',content:'What is the name of my cat?'}], | |
| completion:[],empty:[{role:'user',content:''}], | |
| }; | |
| state.messages=structuredClone(templates[name]??templates.question); | |
| if(name==='math')state.thinking=supportsThinking(); | |
| if(name==='completion'){state.mode='raw';state.raw='The capital of France is';} | |
| renderEditor();changed(); | |
| } | |
| async function switchMode(mode) { | |
| if(mode===state.mode)return; | |
| if(mode==='raw'){ | |
| try {const prepared=await api('/api/prepare',payload());state.raw=prepared.text;}catch {state.raw=state.messages.map(m=>m.content).join('\n');} | |
| } | |
| state.mode=mode;renderEditor();changed(); | |
| } | |
| function insertToken(token) { | |
| const editor=lastEditor?.isConnected?lastEditor:state.mode==='raw'?$('#raw-text'):$('#messages .message:last-child textarea'); | |
| if(!editor||editor.disabled)return; | |
| editor.focus(); | |
| editor.setRangeText(token,editor.selectionStart,editor.selectionEnd,'end'); | |
| editor.dispatchEvent(new Event('input',{bubbles:true})); | |
| } | |
| function renderSpecialTokens(tokens) { | |
| const button=t=>`<button class="${t.text.includes('think')?'think':''}" data-insert-token="${escape(t.text)}" title="ID ${t.id} · special=${t.special}\n${escape(descriptions[t.text]??'An added marker in this checkpoint’s vocabulary.')}" type="button">${escape(t.text)}</button>`; | |
| $('#special-tokens').innerHTML=primaryTokens.map(text=>tokens.find(t=>t.text===text)).filter(Boolean).map(button).join(''); | |
| $('#extra-tokens').innerHTML=tokens.filter(t=>!primaryTokens.includes(t.text)).map(button).join(''); | |
| } | |
| function appendError(message='') { | |
| $('#append-error').textContent=message; | |
| $('#append-error').classList.toggle('hidden',!message); | |
| } | |
| function updateAppendControls() { | |
| const blocked=state.busy||!state.run||state.run.tokens.length>=(state.run.max_tokens??512); | |
| for(const id of ['append-next','write-token']){const button=$(`#${id}`);if(button)button.disabled=blocked;} | |
| const preview=state.appendPreview; | |
| $('#append-text').disabled=state.busy; | |
| $('#append-cancel').disabled=state.busy; | |
| $('#append-custom').disabled=blocked||!preview?.fits||preview.runId!==state.run?.id||preview.text!==$('#append-text').value; | |
| $('#append-custom').textContent='Append first token →'; | |
| } | |
| function closeAppendForm() { | |
| clearTimeout(appendTimer);state.appendVersion++;state.appendPreview=null; | |
| $('#append-form').classList.add('hidden');$('#append-text').value=''; | |
| $('#append-preview').replaceChildren();appendError(); | |
| if(lastEditor?.id==='append-text')lastEditor=null; | |
| $('#write-token')?.setAttribute('aria-expanded','false'); | |
| updateAppendControls(); | |
| } | |
| function openAppendForm() { | |
| if(!state.run||state.busy)return; | |
| $('#append-form').classList.remove('hidden'); | |
| $('#write-token')?.setAttribute('aria-expanded','true'); | |
| queueAppendPreview(); | |
| $('#append-text').focus(); | |
| $('#append-form').scrollIntoView({block:'nearest'}); | |
| } | |
| function queueAppendPreview() { | |
| clearTimeout(appendTimer); | |
| const version=++state.appendVersion,run=state.run,text=$('#append-text').value; | |
| state.appendPreview=null;appendError();$('#append-preview').replaceChildren(); | |
| $('#append-count').textContent=text?'Finding the first token…':'Only the first tokenizer token will be appended. Spaces are preserved.'; | |
| updateAppendControls(); | |
| if(!run||!text)return; | |
| appendTimer=setTimeout(async()=>{ | |
| try { | |
| const result=await api(`/api/runs/${run.id}/append-preview`,{text}); | |
| if(version!==state.appendVersion||state.run!==run)return; | |
| state.appendPreview={...result,text,runId:run.id}; | |
| $('#append-count').textContent=`First token only · ${result.total} / ${result.max_tokens} after append`; | |
| $('#append-preview').innerHTML=result.tokens.map(token=>`<span title="ID ${token.id} · BPE ${escape(token.piece)}">${escape(visible(token.text))}</span>`).join(''); | |
| if(!result.fits)appendError(`The context has reached the ${result.max_tokens}-token limit.`); | |
| } catch(exc){ | |
| if(version!==state.appendVersion||state.run!==run)return; | |
| $('#append-count').textContent='Could not tokenize this text';appendError(exc.message); | |
| } | |
| updateAppendControls(); | |
| },200); | |
| } | |
| async function analyze(endpoint='/api/analyze', customPayload=null) { | |
| if(state.busy)return; | |
| error();state.busy=true;updateRunButton(); | |
| const revision=endpoint==='/api/analyze'?state.revision:state.runRevision; | |
| const started=performance.now(); | |
| $('#busy-notice').classList.remove('hidden'); | |
| $('#busy-text').textContent='Running the forward pass…'; | |
| $('#busy-time').textContent='0 s'; | |
| $('#results').setAttribute('aria-busy','true'); | |
| busyTimer=setInterval(()=>{$('#busy-time').textContent=`${((performance.now()-started)/1000).toFixed(0)} s`;},1000); | |
| try { | |
| const result=await api(endpoint,customPayload??payload()); | |
| state.run=result;state.runRevision=revision;state.query=result.tokens.length-1;state.layer=result.layers-1;state.key=state.query;state.head=state.head==='mean'?'mean':Math.min(state.head,result.heads-1); | |
| state.selectionVersion++;state.selection=null;state.matrix=null;state.matrixCache.clear(); | |
| state.layerAttention=null;state.layerAttentionCache.clear();state.layerAttentionVersion++; | |
| closeAppendForm(); | |
| renderRun(); | |
| $('#stale-notice').classList.toggle('hidden',state.revision===revision); | |
| selectPosition(state.query,state.layer); | |
| } catch(exc) {error(exc.message);if(!$('#append-form').classList.contains('hidden'))appendError(exc.message);} | |
| finally { | |
| state.busy=false;clearInterval(busyTimer);$('#busy-notice').classList.add('hidden'); | |
| $('#results').setAttribute('aria-busy','false');updateRunButton(); | |
| } | |
| } | |
| function tokenTitle(token,index) { | |
| const source=state.run?.token_sources?.[index]; | |
| return `Position ${index} · ID ${token.id}\nText: ${token.text}\nBPE: ${token.piece}${token.special?'\nSpecial token':''}${source==='manual'?'\nManually appended':source==='predicted'?'\nAppended from the model prediction':''}`; | |
| } | |
| function renderRun() { | |
| const run=state.run; | |
| $('#empty-state').classList.add('hidden');$('#run-content').classList.remove('hidden'); | |
| $('#export').disabled=false; | |
| $('#run-label').textContent=`ACTUAL FORWARD PASS / ${run.device.toUpperCase()} / FLOAT32`; | |
| $('#metric-tokens').textContent=run.tokens.length; | |
| $('#metric-input').textContent=`${run.input_count} input${run.added_count?` + ${run.added_count} added`:''}`; | |
| const next=run.predictions.at(-1).ids.at(-1), probability=run.predictions.at(-1).probabilities.at(-1); | |
| $('#metric-next').textContent=visible(run.vocabulary[next].text); | |
| $('#metric-next').title=`ID ${next} · ${run.vocabulary[next].piece}`; | |
| $('#metric-prob').textContent=`${percent(probability)} · no sampling temperature`; | |
| $('#metric-time').textContent=`${run.elapsed.toFixed(1)} s`; | |
| $('#metric-runtime').textContent=`${run.device.toUpperCase()} · eager attention`; | |
| $('#context-tokens').innerHTML=run.tokens.map((token,index)=>`<button class="context-token ${token.special?'special':''} ${index>=run.input_count?'generated':''}" data-query="${index}" title="${escape(tokenTitle(token,index))}"><span class="position">${index}</span><span>${escape(visible(token.text))}</span></button>`).join(''); | |
| $('#generated-legend').classList.toggle('hidden',!run.added_count); | |
| $('#generation-details').classList.toggle('hidden',!run.added_count); | |
| $('#generated-text').textContent=run.continuation_text; | |
| $('#generation-note').textContent=`${run.generated_count} predicted · ${run.manual_count} manually added${run.stopped_on_eos?` · ends with ${run.stop_token}`:''}`; | |
| renderLens(); | |
| if(state.lensColor==='attention')fetchLayerAttention(); | |
| } | |
| function renderLens() { | |
| const run=state.run; | |
| if(!run)return; | |
| const attention=state.lensColor==='attention',ready=!!state.layerAttention; | |
| const amplified=attention&&$('#lens-relative').checked; | |
| const focused=document.activeElement; | |
| const focusSelector=focused?.dataset.cell?`[data-cell="${focused.dataset.cell}"]`:focused?.dataset.column!==undefined?`[data-column="${focused.dataset.column}"]`:['append-next','write-token'].includes(focused?.id)?`#${focused.id}`:null; | |
| updateLensControls(); | |
| let html='<tbody>'; | |
| for(let layer=run.layers-1;layer>=0;layer--){ | |
| const row=attention?state.layerAttention?.weights[layer]:null; | |
| const scale=amplified&&row?Math.max(...row,1e-9):1; | |
| html+=`<tr data-layer="${layer}"><th scope="row" class="row-label"><span>L${String(layer+1).padStart(2,'0')}</span>${layer===run.layers-1?'<small>FINAL</small>':''}</th>`; | |
| for(let key=0;key<run.tokens.length;key++){ | |
| const id=run.predictions[layer].ids[key],p=run.predictions[layer].probabilities[key],token=run.vocabulary[id]; | |
| const weight=attention?(row?.[key]??null):p,masked=attention?maskReason(state.query,key,layer):''; | |
| const relative=weight==null?0:Math.min(1,weight/scale); | |
| const intensity=amplified?Math.log1p(99*relative)/Math.log(100):relative; | |
| const number=masked?'—':weight==null?'…':percent(weight); | |
| const detail=attention?`Attention from position ${state.query}: ${weight==null?'loading':percent(weight)}${masked?' · '+masked:''}`:`Next-token probability: ${percent(p)}`; | |
| const label=state.lens==='probability'?number:escape(visible(token.text)); | |
| html+=`<td><button class="lens-cell ${masked?'lens-masked':''}" data-cell="${layer}:${key}" data-weight="${weight??''}" style="background:${color(intensity)};color:${ink(intensity)}" title="Layer ${layer+1}, position ${key}\nNext token: ${escape(visible(token.text))}\n${escape(detail)}" aria-label="Layer ${layer+1}, position ${key}, predicts ${escape(visible(token.text))}. ${escape(detail)}"><span>${label}</span>${state.lens==='token'?`<small>${number}</small>`:''}</button></td>`; | |
| } | |
| html+='<td class="append-placeholder" aria-hidden="true"></td></tr>'; | |
| } | |
| const next=run.predictions.at(-1).ids.at(-1),nextToken=run.vocabulary[next]; | |
| const nextTitle=`Append ${visible(nextToken.text)} · ID ${next} · ${percent(run.predictions.at(-1).probabilities.at(-1))}. Recompute all layers.`; | |
| html+=`</tbody><tfoot><tr><th scope="row" class="row-label input-label">INPUT ↑</th>${run.tokens.map((token,i)=>`<th scope="col" data-token-header="${i}"><button class="input-column ${token.special?'special':''} ${i>=run.input_count?'generated':''}" data-column="${i}" aria-pressed="${i===state.query}" title="${escape(tokenTitle(token,i))}\nSelect this token. Use Attention to view its weights across layers."><small>#${i}</small><span>${escape(visible(token.text))}</span></button></th>`).join('')}<th scope="col" class="append-column" aria-label="Append a token"><button id="append-next" type="button" title="${escape(nextTitle)}" aria-label="${escape(nextTitle)}"><strong>+</strong><span>${escape(visible(nextToken.text))}</span></button><button id="write-token" type="button" aria-expanded="${!$('#append-form').classList.contains('hidden')}" aria-controls="append-form">Write…</button></th></tr></tfoot>`; | |
| $('#lens-table').innerHTML=html; | |
| updateAppendControls(); | |
| $('.lens-scroll').dataset.colorMode=state.lensColor; | |
| $('.lens-scroll').dataset.labelMode=state.lens; | |
| $('.lens-scroll').setAttribute('aria-busy',attention&&!ready); | |
| markSelection(); | |
| if(focusSelector)$(focusSelector)?.focus({preventScroll:true}); | |
| } | |
| function updateLensControls() { | |
| const attention=state.lensColor==='attention',run=state.run; | |
| $$('[data-lens-color]').forEach(button=>{const active=button.dataset.lensColor===state.lensColor;button.classList.toggle('active',active);button.setAttribute('aria-pressed',active);}); | |
| $$('.lens-head-control, .lens-attention-setting').forEach(el=>el.classList.toggle('hidden',!attention)); | |
| $('#lens-head').value=state.lensHead; | |
| $('#lens-legend-label').textContent=attention?'Attention weight':'Prediction probability'; | |
| $('#lens-scale-min').textContent=attention&&$('#lens-relative').checked?'Low':'0%'; | |
| $('#lens-scale-max').textContent=attention&&$('#lens-relative').checked?'Layer maximum':'100%'; | |
| const peak=attention?layerAttentionPeak():null; | |
| $('#lens-peak').classList.toggle('hidden',!peak); | |
| if(peak){ | |
| $('#lens-peak').textContent=`Show peak: #${peak.key} · ${percent(peak.weight)} ↗`; | |
| $('#lens-peak').title=`Scroll to layer ${peak.layer+1}, key #${peak.key} ${visible(run.tokens[peak.key].text)}. The query stays at #${state.query}.`; | |
| } | |
| const attentionLabels=state.lens==='token-only'?'Cells show predicted tokens; hover for attention weights.':state.lens==='probability'?'Labels show attention weights.':'Cells keep their predicted token; percentages show attention weights.'; | |
| $('#lens-description').textContent=attention?`Each row shows where the selected query token attends in that layer. ${attentionLabels} Future positions are masked.`:'Input tokens stay at the bottom. Read layers upward: layer 1 → final layer. Click a column to select a token. Use Attention to view its weights across layers.'; | |
| if(!attention)$('#lens-status').textContent=state.lens==='token-only'?'Cell color shows the next-token probability. Hover for the exact percentage.':'Cell color and percentage show the next-token probability.'; | |
| else if(run){ | |
| const heads=state.lensHead==='mean'?`mean of all ${run.heads} heads`:`head ${state.lensHead}`; | |
| $('#lens-status').textContent=`${state.layerAttention?'Attention':'Loading attention'} from #${state.query} ${visible(run.tokens[state.query].text)} · ${heads}${$('#lens-relative').checked?' · log colors per layer; percentages stay absolute':''}`; | |
| } | |
| } | |
| function layerAttentionPeak() { | |
| let peak=null; | |
| state.layerAttention?.weights.forEach((row,layer)=>row.forEach((weight,key)=>{ | |
| if(!maskReason(state.query,key,layer)&&(!peak||weight>peak.weight))peak={layer,key,weight}; | |
| })); | |
| return peak; | |
| } | |
| async function fetchLayerAttention() { | |
| const run=state.run,query=state.query,head=state.lensHead; | |
| if(!run||state.lensColor!=='attention')return; | |
| const version=++state.layerAttentionVersion,cacheKey=`${run.id}:${query}:${head}`; | |
| state.layerAttention=state.layerAttentionCache.get(cacheKey)??null; | |
| renderLens(); | |
| if(state.layerAttention)return; | |
| try { | |
| const result=await api(`/api/runs/${run.id}/attention-layers?query=${query}&head=${head}`); | |
| if(version!==state.layerAttentionVersion||state.run!==run||state.lensColor!=='attention')return; | |
| state.layerAttention=result;state.layerAttentionCache.set(cacheKey,result); | |
| if(state.layerAttentionCache.size>24)state.layerAttentionCache.delete(state.layerAttentionCache.keys().next().value); | |
| renderLens(); | |
| } catch(exc){if(version===state.layerAttentionVersion){error(exc.message);$('#lens-status').textContent=`Attention could not be loaded: ${exc.message}`;$('.lens-scroll').setAttribute('aria-busy','false');}} | |
| } | |
| function setLensColor(mode) { | |
| state.lensColor=mode; | |
| if(mode==='attention')fetchLayerAttention(); | |
| else{state.layerAttentionVersion++;renderLens();} | |
| } | |
| function markSelection(reveal=false) { | |
| $$('#context-tokens [data-query]').forEach(el=>{const active=Number(el.dataset.query)===state.query;el.classList.toggle('active',active);el.setAttribute('aria-pressed',active);}); | |
| $$('#lens-table .active, #lens-table .column-selected, #lens-table .active-header').forEach(el=>el.classList.remove('active','column-selected','active-header')); | |
| $(`[data-token-header="${state.query}"]`)?.classList.add('active-header'); | |
| $$('#lens-table [data-column]').forEach(el=>el.setAttribute('aria-pressed',Number(el.dataset.column)===state.query)); | |
| for(let layer=0;layer<state.run.layers;layer++)$(`[data-cell="${layer}:${state.query}"]`)?.classList.add(layer===state.layer?'active':'column-selected'); | |
| $('#layer-select').value=state.layer;$('#head-select').value=state.head; | |
| $('#prev-token').disabled=state.query===0;$('#next-token').disabled=state.query===state.run.tokens.length-1; | |
| if(reveal)revealLensPosition(state.layer,state.query,{vertical:true}); | |
| } | |
| function revealLensPosition(layer,key,{vertical=false}={}) { | |
| const cell=$(`[data-cell="${layer}:${key}"]`),scroller=$('.lens-scroll'); | |
| if(cell){ | |
| const box=cell.getBoundingClientRect(),view=scroller.getBoundingClientRect(); | |
| const labelWidth=$('#lens-table .row-label').getBoundingClientRect().width; | |
| const footer=$('#lens-table tfoot').getBoundingClientRect().height; | |
| const appendWidth=key===state.run.tokens.length-1?$('#lens-table .append-column').getBoundingClientRect().width+2:0; | |
| const reserve=labelWidth+box.width+appendWidth+15<view.width?appendWidth:0; | |
| // Keep preceding keys in view when moving to an earlier causal query. | |
| if(box.right>view.right-reserve-5||box.left<view.left+labelWidth+5)scroller.scrollLeft+=box.right-view.right+reserve+10; | |
| if(vertical){ | |
| if(box.bottom>view.bottom-footer-6)scroller.scrollTop+=box.bottom-view.bottom+footer+10; | |
| else if(box.top<view.top+6)scroller.scrollTop+=box.top-view.top-10; | |
| } | |
| } | |
| } | |
| async function selectPosition(query,layer=state.layer,{reveal=true}={}) { | |
| if(!state.run)return; | |
| const previousQuery=state.query; | |
| state.query=Math.max(0,Math.min(state.run.tokens.length-1,query));state.layer=layer; | |
| const version=++state.selectionVersion; | |
| markSelection(reveal); | |
| // Token chips can select a position outside the currently scrolled columns. | |
| if(!reveal)revealLensPosition(state.layer,state.query); | |
| if(state.lensColor==='attention'&&previousQuery!==state.query)fetchLayerAttention(); | |
| $('#inspector-heading').textContent=`Position ${state.query} / layer ${state.layer+1}`; | |
| const token=state.run.tokens[state.query]; | |
| $('#selected-token').textContent=visible(token.text); | |
| $('#selected-token-detail').textContent=`ID ${token.id} · BPE ${token.piece}${token.special?' · special':''}`; | |
| $('#causal-note').textContent=state.run.layer_types?.[state.layer]==='sliding_attention'?`This layer attends to the current token and preceding tokens within a ${state.run.sliding_window}-token window.`:'This layer attends to the current token and all preceding tokens.'; | |
| $('#top-predictions').textContent='Loading…'; | |
| state.selection=null; | |
| $('#attention-tokens').textContent='Loading attention weights…'; | |
| $('#heads-grid').replaceChildren(); | |
| fetchMatrix(); | |
| try { | |
| const result=await api(`/api/runs/${state.run.id}/inspect?layer=${state.layer}&query=${state.query}`); | |
| if(version!==state.selectionVersion)return; | |
| state.selection=result; | |
| $('#top-predictions').innerHTML=result.top.map(token=>`<div class="prediction-row" style="--probability:${token.probability*100}%" title="ID ${token.id} · BPE ${escape(token.piece)}"><span>${escape(visible(token.text))}</span><span>${percent(token.probability)}</span></div>`).join(''); | |
| renderAttention(); | |
| }catch(exc){if(version===state.selectionVersion){error(exc.message);$('#top-predictions').textContent='No data';}} | |
| } | |
| function setHead(head) { | |
| state.head=head==='mean'?'mean':Number(head);$('#head-select').value=state.head; | |
| if(state.selection)renderAttention(); | |
| fetchMatrix(); | |
| if(state.lensColor==='attention'){state.lensHead=String(head);fetchLayerAttention();} | |
| } | |
| function attentionScale(row) {return $('#relative-scale').checked?Math.max(...row,1e-9):1;} | |
| function selectedAttentionRow() { | |
| const heads=state.selection?.attention; | |
| if(!heads)return null; | |
| return state.head==='mean'?heads[0].map((_,key)=>heads.reduce((sum,row)=>sum+row[key],0)/heads.length):heads[state.head]; | |
| } | |
| function renderAttention() { | |
| if(!state.selection)return; | |
| const run=state.run,row=selectedAttentionRow(),scale=attentionScale(row); | |
| const peak=Math.max(...row),peakIndex=row.indexOf(peak); | |
| const entropy=-row.reduce((sum,p)=>sum+(p>0?p*Math.log2(p):0),0); | |
| const headLabel=state.head==='mean'?`Mean of all ${run.heads} query heads`:`Q ${String(state.head).padStart(2,'0')} → KV ${String(kvHead(state.head)).padStart(2,'0')}`; | |
| $('#head-mapping').textContent=`${headLabel} · GQA ${run.heads}:${run.kv_heads} · ${run.layer_types?.[state.layer]==='sliding_attention'?`Sliding window ${run.sliding_window}`:'Full causal'}`; | |
| $('#attention-matrix-heading').textContent=state.head==='mean'?'Attention matrix · mean of all heads':`Attention matrix · head ${state.head}`; | |
| $('#attention-query').textContent=`QUERY #${state.query} ${visible(run.tokens[state.query].text)}`; | |
| $('#attention-peak').textContent=`Peak: #${peakIndex} · ${percent(peak)} · H ${entropy.toFixed(2)} bit`; | |
| $('#attention-tokens').innerHTML=run.tokens.map((token,index)=>{ | |
| const normalized=row[index]/scale,masked=maskReason(state.query,index); | |
| return `<button class="attention-token ${masked?'masked':''} ${index===state.query?'query-token':''}" data-attention-key="${index}" style="background:${color(normalized)};color:${ink(normalized)}" title="${escape(tokenTitle(token,index))}\nAttention: ${percent(row[index])}${masked?' · '+masked:''}"><span>${escape(visible(token.text))}</span><small>${masked?'—':percent(row[index])}</small></button>`; | |
| }).join(''); | |
| $('#attention-scale-label').textContent=$('#relative-scale').checked?'Color relative to row maximum':'Absolute attention weight'; | |
| $('#attention-scale-max').textContent=percent(scale); | |
| $('#heads-grid').innerHTML=state.selection.attention.map((headRow,h)=>{ | |
| const max=Math.max(...headRow),position=headRow.indexOf(max); | |
| return `<button class="head-card ${h===state.head?'active':''}" data-head="${h}" aria-pressed="${h===state.head}" title="Head ${h} → KV ${kvHead(h)}. Peak: token ${position}, ${percent(max)}"><span class="head-title"><span>Head ${String(h).padStart(2,'0')}</span><span>KV ${kvHead(h)}</span></span><canvas class="head-strip" data-head-strip="${h}" aria-hidden="true"></canvas><span class="head-peak">#${position} ${escape(visible(run.tokens[position].text))} · ${percent(max)}</span></button>`; | |
| }).join(''); | |
| $$('.head-strip').forEach(canvas=>{ | |
| const h=Number(canvas.dataset.headStrip),headRow=state.selection.attention[h],max=attentionScale(headRow); | |
| canvas.width=Math.max(128,run.tokens.length);canvas.height=11; | |
| const ctx=canvas.getContext('2d'),width=canvas.width/run.tokens.length; | |
| headRow.forEach((p,i)=>{ctx.fillStyle=maskReason(state.query,i)?'#efefeb':color(p/max);ctx.fillRect(i*width,0,Math.ceil(width),11);}); | |
| }); | |
| drawMatrix();renderEdge(state.query,state.key); | |
| } | |
| async function fetchMatrix() { | |
| const run=state.run,layer=state.layer,head=state.head; | |
| if(!run)return; | |
| const cacheKey=`${run.id}:${layer}:${head}`; | |
| state.matrix=null;drawMatrix(); | |
| try { | |
| let matrix=state.matrixCache.get(cacheKey); | |
| if(!matrix){ | |
| const response=await fetch(`/api/runs/${run.id}/attention?layer=${layer}&head=${head}`); | |
| if(!response.ok){const body=await response.json();throw new Error(body.detail);} | |
| matrix=new Float32Array(await response.arrayBuffer()); | |
| state.matrixCache.set(cacheKey,matrix); | |
| if(state.matrixCache.size>12)state.matrixCache.delete(state.matrixCache.keys().next().value); | |
| } | |
| if(state.run!==run||state.layer!==layer||state.head!==head)return; | |
| state.matrix=matrix;drawMatrix();renderEdge(state.query,state.key); | |
| }catch(exc){if(state.run===run&&state.layer===layer&&state.head===head)error(exc.message);} | |
| } | |
| let matrixGeometry=null; | |
| function drawMatrix() { | |
| const canvas=$('#attention-matrix');canvas.setAttribute('aria-busy',!state.matrix); | |
| if(!state.run||!canvas.clientWidth)return; | |
| const size=Math.round(canvas.clientWidth),dpr=window.devicePixelRatio||1; | |
| canvas.width=Math.round(size*dpr);canvas.height=Math.round(size*dpr); | |
| const ctx=canvas.getContext('2d');ctx.scale(dpr,dpr); | |
| const left=39,top=34,pad=8,plot=size-left-pad,n=state.run.tokens.length,cell=plot/n; | |
| matrixGeometry={left,top,plot:Math.min(plot,size-top-pad),cell:Math.min(plot,size-top-pad)/n,size}; | |
| const g=matrixGeometry; | |
| ctx.fillStyle='#fff';ctx.fillRect(0,0,size,size); | |
| ctx.font='10px ui-monospace, monospace';ctx.textAlign='center';ctx.fillStyle='#526859'; | |
| ctx.fillText('KEY POSITION →',left+g.plot/2,11); | |
| ctx.save();ctx.translate(7,top+g.plot/2);ctx.rotate(-Math.PI/2);ctx.fillText('QUERY POSITION →',0,0);ctx.restore(); | |
| if(!state.matrix){ctx.fillStyle='#93928b';ctx.fillText('Loading matrix…',size/2,size/2);return;} | |
| const relative=$('#relative-scale').checked; | |
| // Rasterize at one pixel per token pair, then scale. This also handles long contexts efficiently. | |
| const off=document.createElement('canvas');off.width=n;off.height=n; | |
| const offctx=off.getContext('2d'),pixels=offctx.createImageData(n,n); | |
| for(let q=0;q<n;q++){ | |
| let scale=1;if(relative){scale=1e-9;for(let k=0;k<=q;k++)scale=Math.max(scale,state.matrix[q*n+k]);} | |
| for(let k=0;k<n;k++){ | |
| const index=(q*n+k)*4,masked=maskReason(q,k); | |
| const p=masked?0:Math.min(1,state.matrix[q*n+k]/scale); | |
| const rgb=colorRGB(p); | |
| pixels.data[index]=masked?233:rgb[0]; | |
| pixels.data[index+1]=masked?238:rgb[1]; | |
| pixels.data[index+2]=masked?235:rgb[2]; | |
| pixels.data[index+3]=255; | |
| } | |
| } | |
| offctx.putImageData(pixels,0,0);ctx.imageSmoothingEnabled=false;ctx.drawImage(off,left,top,g.plot,g.plot); | |
| ctx.strokeStyle='#e0e3e4';ctx.lineWidth=.5;ctx.strokeRect(left,top,g.plot,g.plot); | |
| const stride=Math.max(1,Math.ceil(n/8));ctx.fillStyle='#93928b'; | |
| for(let i=0;i<n;i+=stride){const pos=(i+.5)*g.cell;ctx.textAlign='center';ctx.fillText(i,left+pos,top-5);ctx.textAlign='right';ctx.fillText(i,left-4,top+pos+2);} | |
| ctx.strokeStyle='#167344';ctx.lineWidth=1.5;ctx.strokeRect(left,top+state.query*g.cell,g.plot,Math.max(1,g.cell)); | |
| ctx.strokeStyle='#bd7748';ctx.lineWidth=1;ctx.strokeRect(left+state.key*g.cell,top+state.query*g.cell,Math.max(g.cell,1),Math.max(g.cell,1)); | |
| } | |
| function renderEdge(query,key) { | |
| if(!state.run)return; | |
| const n=state.run.tokens.length; | |
| key=Math.max(0,Math.min(n-1,key)); | |
| const p=state.matrix?.[query*n+key]??(query===state.query?selectedAttentionRow()?.[key]:null); | |
| $('#edge-detail').innerHTML=`<div>#${query} ${escape(visible(state.run.tokens[query].text))}</div><div>↓</div><div>#${key} ${escape(visible(state.run.tokens[key].text))}</div><strong>${p==null?'—':percent(p)}</strong>${maskReason(query,key)?`<span class="field-note">${maskReason(query,key)}</span>`:''}`; | |
| } | |
| function matrixPoint(event) { | |
| if(!matrixGeometry||!state.matrix)return null; | |
| const rect=$('#attention-matrix').getBoundingClientRect(),g=matrixGeometry; | |
| const x=(event.clientX-rect.left)/rect.width*g.size,y=(event.clientY-rect.top)/rect.height*g.size; | |
| if(x<g.left||y<g.top||x>=g.left+g.plot||y>=g.top+g.plot)return null; | |
| return {query:Math.floor((y-g.top)/g.cell),key:Math.floor((x-g.left)/g.cell)}; | |
| } | |
| $('#messages').addEventListener('input',event=>{ | |
| if(event.target.matches('textarea')){state.messages[Number(event.target.dataset.messageIndex)].content=event.target.value;lastEditor=event.target;changed();} | |
| }); | |
| $('#messages').addEventListener('change',event=>{ | |
| if(event.target.dataset.roleIndex!==undefined){const i=Number(event.target.dataset.roleIndex);state.messages[i].role=event.target.value;event.target.closest('.message').dataset.role=event.target.value;changed();} | |
| }); | |
| $('#messages').addEventListener('click',event=>{ | |
| const button=event.target.closest('[data-remove-message]');if(!button)return; | |
| const i=Number(button.dataset.removeMessage);$('#messages').innerHTML='';state.messages.splice(i,1);renderEditor();changed(); | |
| }); | |
| document.addEventListener('focusin',event=>{if(event.target.matches('#raw-text, #messages textarea, #append-text'))lastEditor=event.target;}); | |
| $('#raw-text').addEventListener('input',event=>{state.raw=event.target.value;lastEditor=event.target;changed();}); | |
| $('#add-message').addEventListener('click',()=>{$('#messages').innerHTML='';state.messages.push({role:state.messages.at(-1)?.role==='user'?'assistant':'user',content:''});renderEditor();changed();$('#messages .message:last-child textarea')?.focus();}); | |
| $('#thinking').addEventListener('change',event=>{state.thinking=event.target.checked;$('#thinking-note').textContent=state.thinking?'The model can start a <think> block':'An empty <think> block precedes the answer';changed();}); | |
| $('#template').addEventListener('change',event=>loadTemplate(event.target.value)); | |
| $('#model-select').addEventListener('change',event=>switchModel(event.target.value)); | |
| $('#reset').addEventListener('click',()=>{$('#template').value='question';loadTemplate('question');}); | |
| $$('[data-mode]').forEach(button=>button.addEventListener('click',()=>switchMode(button.dataset.mode))); | |
| $('#edit-raw').addEventListener('click',async()=>{await switchMode('raw');$('#raw-text').focus();}); | |
| document.addEventListener('click',event=>{const button=event.target.closest('[data-insert-token]');if(button)insertToken(button.dataset.insertToken);}); | |
| $('#analyze').addEventListener('click',()=>analyze()); | |
| document.addEventListener('keydown',event=>{if((event.metaKey||event.ctrlKey)&&event.key==='Enter'){event.preventDefault();if(event.target.closest('#append-form')){if(!$('#append-custom').disabled)$('#append-form').requestSubmit();}else if(!$('#analyze').disabled)analyze();}}); | |
| $('#context-tokens').addEventListener('click',event=>{const token=event.target.closest('[data-query]');if(token)selectPosition(Number(token.dataset.query),state.layer,{reveal:false});}); | |
| $('#lens-table').addEventListener('click',event=>{ | |
| if(event.target.closest('#append-next')){if(state.run)analyze(`/api/runs/${state.run.id}/continue`,{});return;} | |
| if(event.target.closest('#write-token')){openAppendForm();return;} | |
| const column=event.target.closest('[data-column]'); | |
| if(column){selectPosition(Number(column.dataset.column),state.layer,{reveal:false});return;} | |
| const cell=event.target.closest('[data-cell]'); | |
| if(cell){const [layer,query]=cell.dataset.cell.split(':').map(Number);selectPosition(query,layer);} | |
| }); | |
| $$('[data-lens]').forEach(button=>button.addEventListener('click',()=>{state.lens=button.dataset.lens;$$('[data-lens]').forEach(b=>{b.classList.toggle('active',b===button);b.setAttribute('aria-pressed',b===button);});if(state.run)renderLens();})); | |
| $$('[data-lens-color]').forEach(button=>button.addEventListener('click',()=>setLensColor(button.dataset.lensColor))); | |
| $('#lens-head').addEventListener('change',event=>setHead(event.target.value)); | |
| $('#lens-relative').addEventListener('change',()=>renderLens()); | |
| $('#lens-peak').addEventListener('click',()=>{ | |
| const peak=layerAttentionPeak(); | |
| if(peak)revealLensPosition(peak.layer,peak.key,{vertical:true}); | |
| }); | |
| $('#prev-token').addEventListener('click',()=>selectPosition(state.query-1));$('#next-token').addEventListener('click',()=>selectPosition(state.query+1)); | |
| $('#layer-select').addEventListener('change',event=>selectPosition(state.query,Number(event.target.value))); | |
| $('#head-select').addEventListener('change',event=>setHead(event.target.value)); | |
| $('#heads-grid').addEventListener('click',event=>{const head=event.target.closest('[data-head]');if(head)setHead(Number(head.dataset.head));}); | |
| $('#relative-scale').addEventListener('change',()=>{if(state.selection)renderAttention();}); | |
| $('#attention-tokens').addEventListener('click',event=>{const token=event.target.closest('[data-attention-key]');if(token){state.key=Number(token.dataset.attentionKey);drawMatrix();renderEdge(state.query,state.key);}}); | |
| $('#attention-tokens').addEventListener('mouseover',event=>{const token=event.target.closest('[data-attention-key]');if(token)renderEdge(state.query,Number(token.dataset.attentionKey));}); | |
| $('#attention-matrix').addEventListener('mousemove',event=>{const point=matrixPoint(event);if(point)renderEdge(point.query,point.key);}); | |
| $('#attention-matrix').addEventListener('mouseleave',()=>renderEdge(state.query,state.key)); | |
| $('#attention-matrix').addEventListener('click',event=>{const point=matrixPoint(event);if(point){state.key=point.key;selectPosition(point.query);}}); | |
| $('#attention-matrix').addEventListener('keydown',event=>{ | |
| if(!state.run||!['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(event.key))return; | |
| event.preventDefault(); | |
| if(event.key==='ArrowUp'||event.key==='ArrowDown')selectPosition(state.query+(event.key==='ArrowUp'?-1:1)); | |
| else{state.key=Math.max(0,Math.min(state.run.tokens.length-1,state.key+(event.key==='ArrowLeft'?-1:1)));drawMatrix();renderEdge(state.query,state.key);} | |
| }); | |
| $('#append-text').addEventListener('input',queueAppendPreview); | |
| $('#append-cancel').addEventListener('click',()=>{closeAppendForm();$('#write-token')?.focus({preventScroll:true});}); | |
| $('#append-form').addEventListener('submit',event=>{ | |
| event.preventDefault(); | |
| if(!$('#append-custom').disabled)analyze(`/api/runs/${state.run.id}/continue`,{text:$('#append-text').value}); | |
| }); | |
| $('#export').addEventListener('click',()=>{ | |
| if(!state.run)return; | |
| const artifact={...state.run,layer_view:{color_by:state.lensColor,query:state.query,head:state.lensHead,attention:state.lensColor==='attention'?state.layerAttention?.weights:null},selected:{layer:state.layer+1,head:state.head,query:state.query,key:state.key,top:state.selection?.top,attention:state.selection?.attention,head_matrix:state.matrix?Array.from({length:state.run.tokens.length},(_,q)=>Array.from(state.matrix.slice(q*state.run.tokens.length,(q+1)*state.run.tokens.length))):null},method:'softmax(lm_head(final_rmsnorm(layer_output))); final layer normalized once',checkpoint_revision:state.run.revision}; | |
| const url=URL.createObjectURL(new Blob([JSON.stringify(artifact,null,2)],{type:'application/json'})); | |
| const anchor=document.createElement('a');anchor.href=url;anchor.download=`${state.run.model===QWEN_ID?'qwen':'gemma'}-lab-${state.run.id.slice(0,8)}.json`;anchor.click();setTimeout(()=>URL.revokeObjectURL(url),1000); | |
| }); | |
| new ResizeObserver(()=>drawMatrix()).observe($('.matrix-wrap')); | |
| async function pollStatus() { | |
| clearTimeout(statusTimer); | |
| const model=state.model; | |
| try { | |
| const previous=state.status; | |
| const result=await api(`/api/status?model=${encodeURIComponent(model)}`); | |
| if(model!==state.model)return; | |
| state.status=result; | |
| renderModel(result); | |
| $('#model-status').textContent=state.status.message; | |
| $('#model-status').className=`load-status ${state.status.phase==='ready'?'ready':state.status.phase==='error'?'error':''}`; | |
| $('#runtime-note').textContent=state.status.phase==='ready'?`${state.status.device.toUpperCase()} · float32 · eager attention`:'Checkpoint · loading…'; | |
| $('#limit-note').textContent=state.status.max_tokens; | |
| if(state.status.active&&state.busy)$('#busy-text').textContent=state.status.active; | |
| if(state.status.tokenizer_ready&&!previous?.tokenizer_ready){renderSpecialTokens(state.status.special_tokens);await prepare();} | |
| updateRunButton(); | |
| maybeAutoStart(); | |
| }catch(exc){if(model===state.model)$('#model-status').textContent=`Cannot reach the server: ${exc.message}`;} | |
| if(model===state.model)statusTimer=setTimeout(pollStatus,state.busy||state.status?.phase!=='ready'?1500:5000); | |
| } | |
| loadTemplate('question');pollStatus(); | |