import {CheckpointWriter,recoverInterruptedSession} from './recovery.js'; import {runAgent} from './agent.js'; import './styles.css'; import {mountLaunch,refreshLaunch,setLaunchPhase,wireLaunch} from './launch.js'; import { createIcons, Rocket, Plus, SlidersHorizontal, PanelLeftClose, PanelLeftOpen, ArrowUp, ArrowUpRight, ArrowDownToLine, Download, Check, Copy, Code2, FileText, Table2, FileType2, Sparkles, ShieldCheck, Cpu, Zap, Brain, ChevronRight, X, Square, Paperclip, MessageSquare, FolderOpen, RotateCcw, Sun, Moon, ExternalLink, Info, Settings2, Monitor, Smartphone, Maximize2, Eye, Terminal, CircleHelp, LoaderCircle } from 'lucide'; import {DEFAULTS,MODEL,CONTEXTS,KV_FORMATS,kvBytes,formatBytes,modelDownloadBytes,gpuMemoryBytes,escapeHTML as e,validateSettings,TOOLS,SYSTEM,parseXMLCalls,sandboxHTML} from './core.js'; import {Engine} from './engine.js'; import {renderMarkdown,createArtifact,previewMarkup,downloadArtifact} from './artifacts.js'; import {createLaunchScene} from './scene.js'; import {saveSession,loadSessions,hydrate} from './storage.js'; const checkpoints=new CheckpointWriter(saveSession);let checkpointWarning=false; function checkpoint(force=false){return checkpoints.save(session,force).catch(()=>{if(!checkpointWarning){checkpointWarning=true;toast('This browser could not save a recovery checkpoint.');}});} const iconMap={Rocket,Plus,SlidersHorizontal,PanelLeftClose,PanelLeftOpen,ArrowUp,ArrowUpRight,ArrowDownToLine,Download,Check,Copy,Code2,FileText,Table2,FileType2,Sparkles,ShieldCheck,Cpu,Zap,Brain,ChevronRight,X,Square,Paperclip,MessageSquare,FolderOpen,RotateCcw,Sun,Moon,ExternalLink,Info,Settings2,Monitor,Smartphone,Maximize2,Eye,Terminal,CircleHelp,LoaderCircle}; const icons=()=>createIcons({icons:iconMap,attrs:{'stroke-width':1.65}}); const i=name=>``; const $=q=>document.querySelector(q); let settings;try{settings=validateSettings(JSON.parse(localStorage.getItem('minicpm5-settings'))||DEFAULTS);}catch{settings={...DEFAULTS};} let session={id:crypto.randomUUID(),title:'New conversation',messages:[],history:[],updatedAt:Date.now()},sessions=[],busy=false,controller=null,scene,selectedArtifact=null,loadingPromise=null,lastPaint=0,lastFocus=null; const prompts=[ {kind:'html',icon:'code-2',label:'Build a website',desc:'An idea, turned into a live page',prompt:'Create a polished, responsive HTML landing page for an independent coffee shop called Orbit Coffee. Give it warm colors, beautiful typography, a menu, and a working dark mode toggle. Use the create_html tool.'}, {kind:'document',icon:'file-text',label:'Write a document',desc:'From a blank page to a first draft',prompt:'Create a Word document with a thoughtful, practical 30-day plan for learning Python, with weekly goals, exercises, and a final project. Use the create_document tool.'}, {kind:'spreadsheet',icon:'table-2',label:'Make a spreadsheet',desc:'Put your numbers into perspective',prompt:'Create an Excel budget tracker for a small creative studio. Include monthly income and expense examples, formulas for totals and profit, and clear categories. Use the create_spreadsheet tool.'}, {kind:'pdf',icon:'file-type-2',label:'Design a PDF',desc:'Something worth saving and sharing',prompt:'Create a polished PDF field guide to stargazing for beginners. Include what to bring, how to find your first constellations, and a practical observation checklist. Use the create_pdf tool.'} ]; const artifactIcons={html:'code-2',document:'file-text',spreadsheet:'table-2',pdf:'file-type-2'}; $('#app').innerHTML=`
Playground / MiniCPM5-2BQ8_0
${i('shield-check')}100% local
YOUR ENGINE IS READY

What will you create?

Think it through. Make it real.

SMALL MODEL. SERIOUS CAPABILITY.

A little engine.
For big ideas.

Meet MiniCPM5. Your private creative partner
for thinking, building, and getting things done.

2B class8-bit precisionUp to 128K context
2.68 GB · this session only
${storageChoice('save-model')}
42 LAYERS ↗LOCAL INTELLIGENCEMINI / 005
${i('code-2')}Websites${i('file-text')}Documents${i('table-2')}Spreadsheets${i('file-type-2')}PDFs
Go from “what if” to “here it is.”4 TOOLS. ENDLESS POSSIBILITIES.
${prompts.map(p=>``).join('')}
${i('shield-check')}Your conversations and creations stay on your device. No account or API key needed.
MiniCPM5-2B
${i('shield-check')}Created locally by MiniCPM5
`; mountLaunch();icons(); const engine=new Engine(onStatus);scene=createLaunchScene($('#launch-scene')); document.body.classList.toggle('hf-embedded',window.self!==window.top);document.body.classList.add('launch-view');$('.breadcrumb').firstChild.textContent='Launchpad '; function enterPlayground(){document.body.classList.remove('launch-view','gpu-preparing');$('.breadcrumb').firstChild.textContent='Playground ';$('#settings-panel').hidden=true;$('#loading-panel').hidden=true;renderMessages();scene.setMode('paused');$('#prompt').focus();} function toast(message){$('#toast').textContent=message;$('#toast').hidden=false;clearTimeout(toast.timer);toast.timer=setTimeout(()=>$('#toast').hidden=true,4200);} function storeSettings(){settings=validateSettings(settings);try{localStorage.setItem('minicpm5-settings',JSON.stringify(settings));}catch{}syncToggles();} function storageChoice(id){return `
${settings.saveModel?`Saves ${formatBytes(modelDownloadBytes(settings))} for faster future launches.`:'Off · downloads each visit, nothing saved.'}
`;} function syncStorage(){refreshLaunch(settings,engine.loading);for(const id of ['save-model','save-model-setting']){const button=$('#'+id);if(button){button.setAttribute('aria-checked',String(settings.saveModel));button.nextElementSibling.textContent=settings.saveModel?`Saves ${formatBytes(modelDownloadBytes(settings))} for faster future launches.`:'Off · downloads each visit, nothing saved.';}}if(!engine.ready&&!engine.loading)$('#load-caption').textContent=`${formatBytes(modelDownloadBytes(settings))} · ${settings.saveModel?'optional local storage':'this session only'}`;} async function setModelStorage(value){if(engine.loading||busy){syncStorage();return toast('Finish the current operation first.');}settings.saveModel=value;storeSettings();if(!value){try{await engine.clearCache();toast('Model storage off. Saved weights removed.');}catch{toast('Storage is off. Could not remove the older copy; use Remove downloaded model in settings.');}}else toast(engine.ready?'Storage enabled for the next model load.':'Model storage enabled · faster future launches.');if(!$('#settings-panel').hidden)renderSettings();} function syncToggles(){syncStorage();for(const key of ['thinking','tools']){$(`#${key}-toggle`).classList.toggle('on',settings[key]);$(`#${key}-toggle`).setAttribute('aria-pressed',String(settings[key]));}} function setStatus(message){$('#generation-status').textContent=message;} let startLoad=0,loadTimer; function onStatus(s){ if(s.diagnostic){ const output=$('#load-diagnostics');if(output)output.textContent=engine.logs.slice(-16).map(l=>l.line).join('\n'); if(document.body.classList.contains('gpu-preparing')){ let detail=''; if(/dedicated.*layer/i.test(s.diagnostic))detail=s.diagnostic; if(/DSpark/i.test(s.diagnostic))detail='Preparing DSpark draft verification'; if(/loading model|model buffer/i.test(s.diagnostic))detail='Loading model tensors onto the GPU'; if(/KV.*buffer|KV.*size/i.test(s.diagnostic))detail='Allocating the KV cache'; if(/warmup|warming/i.test(s.diagnostic))detail='Warming up the inference shaders'; if(/offloaded.*layers/i.test(s.diagnostic))detail='Model on GPU · preparing the context'; if(detail)$('#load-stage').textContent=detail; } return; } if(s.phase==='download'||s.phase==='compile'){scene.setMode(s.phase==='compile'?'compile':'loading');document.body.classList.toggle('gpu-preparing',s.phase==='compile');if(document.body.classList.contains('launch-view'))setLaunchPhase('loading');} $('#loading-panel').hidden=false;$('#load-stage').textContent=s.label; scene.setProgress(s.progress||0);$('#load-progress').style.width=`${s.phase==='ready'?100:s.phase==='compile'?100:Math.min(100,(s.progress||0)*100)}%`; $('#load-progress').classList.toggle('preparing',s.phase==='compile'); $('#load-progress-text').textContent=s.phase==='compile'?'GPU':s.phase==='ready'?'Ready':`${Math.floor((s.progress||0)*100)}%`; if(s.phase==='download')$('#load-detail').textContent=`${formatBytes(s.loaded||0)} / ${formatBytes(s.total||modelDownloadBytes(settings))}${s.elapsed>2?' · '+formatBytes((s.loaded||0)/s.elapsed)+'/s':''} · ${settings.saveModel?'saved on your device':'this session only · not saved'}`; if(s.phase==='compile')$('#load-detail').textContent=s.cached?'Weights restored from local cache. Preparing inference.':'Download complete. Initializing inference; the runtime steps appear below.'; for(const p of ['download','compile','ready'])$(`#stage-${p}`).classList.toggle('current',s.phase===p); if(s.phase==='ready'){ if(import.meta.env.DEV)fetch('/__qa/file/load-timing.json',{method:'POST',body:JSON.stringify({seconds:engine.loadSeconds,downloadSeconds:engine.downloadSeconds,gpuSeconds:engine.gpuSeconds,downloadMetrics:engine.downloadMetrics,settings:engine.settings,gpuLayers:engine.gpuLayers,runtimeCapabilities:engine.runtimeCapabilities,speculation:engine.dedicated?.specStats})}).catch(()=>{}); clearInterval(loadTimer);if(document.body.classList.contains('launch-view')){scene.setMode('launch');setTimeout(enterPlayground,matchMedia('(prefers-reduced-motion: reduce)').matches?100:1250);}else scene.setMode('idle');$('#load-model').innerHTML=`${i('check')}Ready to create`;$('#load-model').disabled=true;$('#load-caption').textContent=`${s.cached?'Warm':'First'} load · ${s.seconds.toFixed(1)} seconds`; $('#load-detail').textContent=`${s.info.n_layer} layers · ${s.info.n_ctx.toLocaleString()} tokens · ${settings.cacheK.toUpperCase()} KV`; $('#model-led').classList.add('ready');$('#runtime-status').textContent=`Dedicated WebGPU · ${(settings.context/1024)}K · ${settings.cacheK.toUpperCase()} KV${engine.runtimeCapabilities?.speculative?' · DSpark':''}`; $('#device-status').textContent='Model loaded on this device';icons(); } } async function loadModel(){ if(engine.ready)return; if(loadingPromise)return loadingPromise; loadingPromise=(async()=>{ document.body.classList.add('launch-view');$('#ml-loader').append($('.ml-runtime-details'));$('.ml-runtime-details').open=false;toggleSettings(false);setLaunchPhase('loading');$('#cancel-load').disabled=false;$('#cancel-load').textContent='CANCEL LOADING'; $('#load-model').disabled=true;for(const id of ['save-model','save-model-setting'])if($('#'+id))$('#'+id).disabled=true;$('#load-model').innerHTML=`${i('loader-circle')}Launching…`;icons();$('#load-error').hidden=true; document.body.classList.remove('gpu-preparing');$('#load-diagnostics').textContent='Waiting for runtime startup…';scene.setMode('loading');startLoad=performance.now();loadTimer=setInterval(()=>$('#load-elapsed').textContent=`${Math.floor((performance.now()-startLoad)/1000)}s elapsed`,1000); try{const load=engine.load(settings);renderSettings();await load;}catch(err){ const runtimeMismatch=/Memory64|CompileError|LinkError|WebAssembly.*parse|failed to.*wasm/i.test(err.message); const guidance=runtimeMismatch?'The browser rejected the runtime binary. This is a runtime compatibility issue; reducing context does not fix it. Reload the Space to get the latest runtime.':/out of memory|allocation|allocate|GPU.*memory/i.test(err.message)?'Try a smaller context or lower KV precision.':'Check Runtime progress for the failing step and try again.'; clearInterval(loadTimer);document.body.classList.remove('gpu-preparing');setLaunchPhase('idle');$('#load-error').hidden=err.name==='AbortError';$('#load-error').textContent=`Could not load the model: ${err.message}. ${engine.sessionFiles.size?'Weights remain in this tab for retry. ':''}${guidance}`; if(err.name!=='AbortError'){$('#ml-error').append($('.ml-runtime-details'));$('.ml-runtime-details').open=true;}$('#load-model').disabled=false;$('#load-model').innerHTML=`${i('rotate-ccw')}Try again`;scene.setMode('idle');icons();throw err; }finally{loadingPromise=null;refreshLaunch(settings,false);for(const id of ['save-model','save-model-setting'])if($('#'+id))$('#'+id).disabled=false;if(!$('#settings-panel').hidden)renderSettings();} })();return loadingPromise; } $('#load-model').onclick=()=>loadModel().catch(()=>{}); $('#save-model').onclick=()=>setModelStorage(!settings.saveModel); async function persist(){if(!session.messages.length)return;try{await checkpoints.save(session,true);sessions=(await loadSessions()).map(recoverInterruptedSession);renderHistory();}catch{toast('Browser storage is full or unavailable. Download anything you want to keep.');}} function renderHistory(){ $('#history-list').innerHTML=sessions.length?sessions.slice(0,30).map(s=>``).join(''):'

Your ideas start here.

';icons(); } function allArtifacts(){return session.messages.flatMap(m=>m.artifacts||[]);} function renderArtifactCard(a){ let mini=''; if(a.type==='html')mini=``; else if(a.type==='spreadsheet'){const sh=a.args.sheets[0];mini=`
${sh.columns.slice(0,4).map(c=>``).join('')}${sh.rows.slice(0,3).map(r=>`${r.slice(0,4).map(c=>``).join('')}`).join('')}
${e(c)}
${e(c??'')}
`;} else mini=`
${a.type==='pdf'?'PDF / FIELD NOTES':'DOCUMENT'}${e(a.title)}
${renderMarkdown(a.args.markdown.slice(0,420))}
`; return `
${i('check')}Ready to explore
`; } function splitThinking(m){let content=m.content||'',reasoning=m.reasoning||'';if(content.includes('')){const end=content.indexOf('');const start=content.indexOf('');reasoning+=end>=0?content.slice(start+7,end):content.slice(start+7);content=end>=0?content.slice(end+8):content.slice(0,start);}const functionStart=content.indexOf('=0)content=content.slice(0,functionStart);return {content,reasoning};} function renderMessage(m){ if(m.role==='user')return `
${e(m.content)}
`; const {content,reasoning}=splitThinking(m); return `
${i('rocket')}
MiniCPM5 LOCAL${m.generating?'working•••':''}
${reasoning?`
${i('brain')}${m.generating?'Thinking it through':'Thought process'}
${renderMarkdown(reasoning)}
`:''}
${renderMarkdown(content)}
${m.toolName?`
${i('loader-circle')}Creating ${e(m.toolName.replace('create_',''))}…
`:''}${(m.artifacts||[]).map(renderArtifactCard).join('')}${m.error?`
${e(m.error)}
`:''}${m.interrupted?`
${session.interrupted?'':''}
`:''}${m.stats?`
${i('zap')}${m.stats.tps?m.stats.tps.toFixed(1)+' tok/s':'Completed'}${m.stats.tokens||0} tokens${m.stats.seconds.toFixed(1)}s${m.stats.ttft?`${m.stats.ttft.toFixed(2)}s to first token`:''}${m.stats.finish==='length'?'Response limit reached':''}
`:''}
`; } function renderMessages(){ const scrolling=$('#scroll-area'),nearBottom=scrolling.scrollHeight-scrolling.scrollTop-scrolling.clientHeight<140; const hasMessages=session.messages.length>0;$('#welcome').hidden=hasMessages;$('#messages').hidden=!hasMessages; $('#messages').innerHTML=session.messages.map(renderMessage).join('');$('#artifact-count').textContent=allArtifacts().length;scene.setMode(hasMessages?'paused':engine.loading?'loading':'idle');icons(); if(nearBottom)scrolling.scrollTop=scrolling.scrollHeight; } function repaint(force=false){ if(!force&&performance.now()-lastPaint<140)return; lastPaint=performance.now();const m=session.messages.at(-1),target=m&&document.querySelector(`[data-message="${m.id}"]`); if(!target){renderMessages();return;} const area=$('#scroll-area'),atBottom=area.scrollHeight-area.scrollTop-area.clientHeight<150; target.outerHTML=renderMessage(m);$('#artifact-count').textContent=allArtifacts().length;icons(); if(atBottom)area.scrollTop=area.scrollHeight; } async function generate(){ let m,started=0,first=0;const failedMessages=[]; try{ await runAgent({chat:engine.chat.bind(engine),history:session.history,settings:{...settings,context:engine.info.n_ctx},signal:controller.signal,createArtifact,onEvent:async event=>{ if(event.type==='start'){m={id:crypto.randomUUID(),role:'assistant',content:'',reasoning:'',artifacts:[],generating:true};session.messages.push(m);started=performance.now();first=0;session.pendingGeneration={startedAt:Date.now(),messageId:m.id};await checkpoint(true);repaint(true);} if(event.type==='chunk'){ const chunk=event.chunk,delta=chunk.choices?.[0]?.delta||{}; if((delta.content||delta.reasoning_content||delta.tool_calls)&&!first)first=performance.now(); m.content+=delta.content||'';m.reasoning+=delta.reasoning_content||''; const rawName=m.content.match(/c.function?.name)?.function.name||rawName||m.toolName; if(chunk.prompt_progress){const p=chunk.prompt_progress;setStatus(`Reading your prompt · ${p.processed.toLocaleString()} / ${p.total.toLocaleString()} tokens${p.cache?' · '+p.cache+' cached':''}`);} else if(first)setStatus(m.toolName?`Using ${m.toolName.replace('create_','')} · writing your file`:`Writing on your GPU${chunk.timings?.predicted_per_second?' · '+chunk.timings.predicted_per_second.toFixed(1)+' tok/s':''}`); checkpoint();repaint(); } if(event.type==='response'){m.generating=false;m.toolName=null;m.stats={tokens:event.tokens||event.timings?.predicted_n||0,tps:event.timings?.predicted_per_second,seconds:(performance.now()-started)/1000,ttft:first?(first-started)/1000:0,finish:event.finish};} if(event.type==='tool-start'){m.toolName=event.name;setStatus(`Creating your ${event.name.replace('create_','')} locally…`);repaint(true);} if(event.type==='tool-success'){m.artifacts.push(event.artifact);if(event.recovered){for(const prior of failedMessages)prior.error='The model corrected this tool call and created the file below.';renderMessages();}} if(event.type==='tool-error'){m.error=event.retrying?`The tool returned an error: ${event.error}. MiniCPM5 is correcting it automatically…`:`The tool could not finish: ${event.error}`;failedMessages.push(m);} if(event.type==='tools-complete'){session.pendingGeneration={...session.pendingGeneration,messageId:null,phase:'followup'};m.toolName=null;m.content=event.content;repaint(true);await persist();} if(event.type==='done'&&event.finish==='length')m.error='The response reached your output limit. Ask “continue” or increase Maximum response tokens in settings.'; if(event.type==='limit'){if(m)m.error='The model reached its repair limit. Your conversation and any completed files are saved; send another message to continue.';} }}); }catch(error){ if(m){m.generating=false;m.toolName=null;m.error=error.name==='AbortError'||controller.signal.aborted?'Generation stopped.':/context|kv_cache|too large|exceed/i.test(error.message)?'This conversation exceeds the context window. Start a new conversation or choose a larger context in Engine room.':`Generation failed: ${error.message}`;} }finally{if(m)m.generating=false;session.pendingGeneration=null;session.interrupted=false;await checkpoint(true);repaint(true);} } let attachment=null,submitting=false; $('#composer').onsubmit=async ev=>{ ev.preventDefault();if(busy){controller?.abort();return;}if(submitting)return; let text=$('#prompt').value.trim();if(!text&&!attachment)return;submitting=true; if(!engine.ready){setStatus('Launching your local model before sending…');try{await loadModel();}catch{setStatus('Model could not load. Your draft is still here.');submitting=false;return;}} if(attachment)text+=`\n\nAttached text file: ${attachment.name}\n\n${attachment.text}`; $('#prompt').value='';$('#prompt').style.height='auto';attachment=null;$('#attachment-preview').hidden=true; if(!session.messages.length)session.title=text.slice(0,55); session.messages.push({id:crypto.randomUUID(),role:'user',content:text});session.history.push({role:'user',content:text});busy=true;submitting=false;controller=new AbortController();$('#send').innerHTML=i('square');$('#send').setAttribute('aria-label','Stop generation');$('#prompt').disabled=true;scene.setMode('paused');renderMessages(); session.pendingGeneration={startedAt:Date.now(),messageId:null};await checkpoint(true);try{await generate();}finally{busy=false;$('#send').innerHTML=i('arrow-up');$('#send').setAttribute('aria-label','Send message');$('#prompt').disabled=false;setStatus('');icons();await persist();$('#prompt').focus();} }; $('#prompt').addEventListener('keydown',ev=>{if(ev.key==='Enter'&&!ev.shiftKey&&!ev.isComposing){ev.preventDefault();$('#composer').requestSubmit();}}); $('#prompt').addEventListener('input',()=>{$('#prompt').style.height='auto';$('#prompt').style.height=Math.min(180,$('#prompt').scrollHeight)+'px';}); async function newChat(){if(busy||engine.loading){toast('Stop the current generation or wait for loading to finish first.');return;}session.interrupted=false;await persist();$('.recovery-notice')?.remove();session={id:crypto.randomUUID(),title:'New conversation',messages:[],history:[],updatedAt:Date.now()};renderMessages();renderHistory();$('#prompt').focus();} $('#new-chat').onclick=newChat;$('#chat-nav').onclick=()=>{$('#main').scrollIntoView();}; document.addEventListener('click',async ev=>{ const draft=ev.target.closest('[data-download-draft]');if(draft){const message=session.messages.find(m=>m.id===draft.dataset.downloadDraft);if(message){const blob=new Blob([message.content||message.reasoning||''],{type:'text/plain;charset=utf-8'}),url=URL.createObjectURL(blob);downloadArtifact({url,filename:'MiniCPM5-interrupted-draft.txt'});setTimeout(()=>URL.revokeObjectURL(url),30000);}return;} if(ev.target.closest('[data-retry-reply]')){if(busy||engine.loading)return;session.interrupted=false;$('#prompt').value='The browser interrupted your previous reply. Please complete the previous request. If a file was incomplete, call its creation tool again with complete content.';$('#composer').requestSubmit();return;} const prompt=ev.target.closest('[data-prompt]');if(prompt){const p=prompts.find(p=>p.kind===prompt.dataset.prompt);$('#prompt').value=p.prompt;$('#prompt').focus();return;} const history=ev.target.closest('[data-session]');if(history){if(busy||engine.loading)return toast('Finish the current operation first.');await persist();const found=sessions.find(s=>s.id===history.dataset.session);if(found){session=hydrate(structuredClone(found));renderMessages();renderHistory();}return;} const view=ev.target.closest('[data-view]');if(view){const a=allArtifacts().find(a=>a.id===view.dataset.view);if(a)openArtifact(a);return;} const dl=ev.target.closest('[data-download]');if(dl){const a=allArtifacts().find(a=>a.id===dl.dataset.download);if(a)downloadArtifact(a);return;} const copy=ev.target.closest('[data-copy]');if(copy){const m=session.messages.find(m=>m.id===copy.dataset.copy);if(m)copyText(splitThinking(m).content);} }); async function copyText(text){try{await navigator.clipboard.writeText(text);toast('Copied to clipboard');}catch{toast('Clipboard access is unavailable. Select the source text and copy it.');}} function openArtifact(a){selectedArtifact=a;lastFocus=document.activeElement;$('#dialog-title').textContent=a.title;$('#dialog-filename').textContent=a.filename;$('#dialog-icon').innerHTML=i(artifactIcons[a.type]);$('#dialog-icon').className=`artifact-card-icon ${a.type}`;$('#artifact-preview').innerHTML=previewMarkup(a);$('#artifact-code code').textContent=a.args.html||a.args.markdown||JSON.stringify(a.args.sheets,null,2);$('#code-tab').innerHTML=i('code-2')+(a.type==='html'?'Code':a.type==='spreadsheet'?'Data':'Markdown');$('#artifact-preview-note').textContent=a.type==='html'?'Sandboxed preview · network access disabled':a.type==='document'?'Content preview · Word layout may differ':'';$('#artifact-preview').classList.remove('mobile');setArtifactTab(false);$('#info-dialog').close();$('#artifact-dialog').showModal();icons();} function setArtifactTab(code){$('#artifact-code').hidden=!code;$('#artifact-preview').hidden=code;$('#preview-tab').classList.toggle('active',!code);$('#code-tab').classList.toggle('active',code);} $('#preview-tab').onclick=()=>setArtifactTab(false);$('#code-tab').onclick=()=>setArtifactTab(true);$('#dialog-download').onclick=()=>selectedArtifact&&downloadArtifact(selectedArtifact);$('#copy-code').onclick=()=>copyText($('#artifact-code code').textContent);$('#dialog-close').onclick=()=>{$('#artifact-dialog').close();$('#artifact-preview').innerHTML='';lastFocus?.focus();};$('#artifact-dialog').addEventListener('close',()=>{$('#artifact-preview').innerHTML='';}); $('#mobile-preview').onclick=()=>{$('#artifact-preview').classList.add('mobile');$('#mobile-preview').classList.add('active');$('#desktop-preview').classList.remove('active');};$('#desktop-preview').onclick=()=>{$('#artifact-preview').classList.remove('mobile');$('#desktop-preview').classList.add('active');$('#mobile-preview').classList.remove('active');}; $('#artifacts-nav').onclick=()=>{const artifacts=allArtifacts();$('#info-content').innerHTML=`YOUR WORKSPACE

Your creations

Files from this conversation, saved on this device.

${artifacts.length?artifacts.map(renderArtifactCard).join(''):'
'+i('folder-open')+'Make your first creation

Ask MiniCPM5 to build a website, write a document, make a spreadsheet, or design a PDF.

'}`;$('#info-dialog').showModal();icons();}; $('#info-close').onclick=()=>$('#info-dialog').close(); $('#about').onclick=()=>{$('#info-content').innerHTML=`LITTLE ENGINE. BIG IDEAS.

Intelligence, on your terms.

This playground runs OpenBMB's MiniCPM5-2B directly on your GPU. No inference server, API keys, account, or usage fees.

2.52Bparameters · 42 layers
Q8_0official 8-bit weights
128Knative context ceiling
4 toolsfiles created locally

Made to make things

HTML runs in an isolated preview with network access disabled. Documents download as Word files, workbooks as Excel files, and PDFs as real paginated documents. Model outputs may need editing.

Your device is the engine

Current desktop Chrome or Edge is recommended, with at least 8 GB system or unified memory and enough GPU memory for the model plus your chosen context. The dedicated WebGPU runtime uses the original 2.68 GB Q8 weights. DSpark adds a 648 MB draft model and adapts to response speed. Downloads are kept only for the current session by default. Enable Save model in browser for faster future launches using optional local storage. Safari and Firefox compatibility and speed vary by device.

Chats and files are saved in this browser. They are not sent to a model server. Hugging Face serves the app and weights and receives those download requests. Clearing site data removes local chats and downloads.

`;$('#info-dialog').showModal();icons();}; function renderSettings(){ $('#settings-panel h2').textContent=document.body.classList.contains('launch-view')?'Launch settings':'Engine room'; const select=(id,label,values,current,hint='')=>``; const range=(id,label,min,max,step,value)=>``; const estimate=gpuMemoryBytes(settings); $('#settings-content').innerHTML=`
${i('rocket')}
MiniCPM5-2BOfficial Q8_0 · 2.68 GB
8-BIT
${storageChoice('save-model-setting')}${select('context-setting','Context window',CONTEXTS.map(c=>[c,`${c.toLocaleString()} tokens · ${c/1024}K`]),settings.context,'Includes instructions, conversation, tool definitions and the response.') }${select('cacheK-setting','KV cache precision',Object.entries(KV_FORMATS).map(([k,v])=>[k,v.name]),settings.cacheK,'One precision for both the key and value cache.')}
Estimated GPU memory${formatBytes(estimate)}
WeightsKV cache ${formatBytes(kvBytes(settings.context,settings.cacheK,settings.cacheV))}
Plus browser / driver overhead. Available VRAM cannot be measured reliably. Larger windows use more memory.
${select('dspark-setting','DSpark acceleration',[['auto','Auto · use when faster'],['on','On · always draft ahead'],['off','Off · base model only']],settings.dspark,'Adds 648 MB of draft weights. The full Q8 model verifies every token. Auto switches to ordinary decoding when drafting is slower.')}${select('performance-setting','Prompt processing',[['balanced','Balanced · smaller prompt batches'],['fast','Fast · larger GPU batches']],settings.performance)}

Memory and batching changes take effect when you reload the model. FP16 KV is recommended for speed. Q8 / Q4 save memory; Q4 has a larger accuracy tradeoff. Attention uses fused GPU kernels.

Generation & instructions${range('temperature-setting','Temperature',0,2,.05,settings.temperature)}${range('topP-setting','Top P',.05,1,.01,settings.topP)}${range('topK-setting','Top K',0,200,1,settings.topK)}

0 uses full-vocabulary sampling, which is slower and disables DSpark.

${range('repeatPenalty-setting','Repeat penalty',.8,1.5,.01,settings.repeatPenalty)}${select('maxTokens-setting','Maximum response tokens',[...new Set([128,512,1024,2048,4096,8192,16384,32768,settings.maxTokens])].sort((a,b)=>a-b).filter(n=>n[n,n.toLocaleString()]),settings.maxTokens,'Thinking and tool arguments share this response budget.')}

Checking browser cache…

Runtime details
${e(JSON.stringify({runtime:engine.runtimeName||'MiniCPM5 dedicated WebGPU',loaded:engine.ready,loadSeconds:engine.loadSeconds,downloadSeconds:engine.downloadSeconds,gpuSeconds:engine.gpuSeconds,downloadRequests:engine.downloadMetrics,context:engine.info?.n_ctx,runtimeCapabilities:engine.runtimeCapabilities,speculation:engine.dedicated?.specStats,browser:navigator.userAgent,gpuLayersReported:engine.gpuLayers||'not reported by runtime',gpuLayersRequested:43,crossOriginIsolated,adapter:engine.adapter?.info?.description||engine.adapter?.info?.vendor},null,2))}
`; icons(); const read=()=>{for(const key of ['context','cacheK','dspark','performance','temperature','topP','topK','repeatPenalty','maxTokens','seed','system']){const el=$(`#${key}-setting`);settings[key]=['cacheK','cacheV','dspark','performance','system'].includes(key)?el.value:Number(el.value);}storeSettings();}; for(const input of $('#settings-content').querySelectorAll('input,select,textarea'))input.addEventListener('input',()=>{const out=$(`#${input.id}-value`);if(out)out.value=input.value;if(['context-setting','cacheK-setting','dspark-setting'].includes(input.id)){const c=+$('#context-setting').value,k=$('#cacheK-setting').value,v=k;$('#memory-estimate').textContent=formatBytes(gpuMemoryBytes({...settings,context:c,cacheK:k,cacheV:v,dspark:$('#dspark-setting').value}));$('#kv-estimate').textContent=formatBytes(kvBytes(c,k,v));}if(!engine.ready||!['context-setting','cacheK-setting','dspark-setting','performance-setting'].includes(input.id)){const key=input.id.replace('-setting','');settings[key]=['system','cacheK','cacheV','dspark','performance'].includes(key)?input.value:Number(input.value);storeSettings();}}); $('#apply-settings').onclick=async()=>{if(busy||engine.loading)return toast('Finish the current operation first.');read();if(engine.ready){await engine.unload();$('#load-model').disabled=false;$('#model-led').classList.remove('ready');await newChat();try{await loadModel();}catch{}}toast('Settings applied');renderSettings();}; for(const input of $('#settings-content').querySelectorAll('input,select,textarea'))input.disabled=busy||engine.loading; $('#save-model-setting').disabled=busy||engine.loading;$('#save-model-setting').onclick=()=>setModelStorage(!settings.saveModel); $('#reset-settings').onclick=async()=>{if(busy||engine.loading)return;settings={...DEFAULTS};storeSettings();await setModelStorage(false);renderSettings();toast('Defaults restored. Apply & reload to change allocated memory.');}; $('#clear-cache').onclick=async()=>{if(busy||engine.loading)return toast('Finish the current operation first.');await engine.clearCache();$('#cache-info').textContent='Downloaded weights removed. Your chats and creations are kept.';toast('Model cache removed');}; $('#export-chat').onclick=()=>{const blob=new Blob([JSON.stringify({...session,messages:session.messages.map(m=>({...m,artifacts:m.artifacts?.map(({blob,url,...a})=>a)}))},null,2)],{type:'application/json'});const url=URL.createObjectURL(blob);downloadArtifact({url,filename:'minicpm5-conversation.json'});setTimeout(()=>URL.revokeObjectURL(url),30000);}; engine.cacheState(settings).then(c=>{if($('#cache-info'))$('#cache-info').textContent=c.cached?`${formatBytes(c.bytes)} stored on this device.${settings.saveModel?' Ready for a warm start.':' Storage is off; remove this earlier copy below.'}`:c.bytes?`${formatBytes(c.bytes)} in completed downloads. Remove below to free the space.`:settings.saveModel?`No weights saved yet. Your next load will save ${formatBytes(modelDownloadBytes(settings))}.`:'Model storage is off. Weights download for this session only.';}).catch(()=>{if($('#cache-info'))$('#cache-info').textContent='Browser cache unavailable.';}); } function toggleSettings(open){$('#settings-panel').hidden=open===undefined?!$('#settings-panel').hidden:!open;if(!$('#settings-panel').hidden)renderSettings();} $('#settings-toggle').onclick=()=>toggleSettings();$('#settings-close').onclick=()=>toggleSettings(false); $('#sidebar-toggle').onclick=()=>document.body.classList.toggle('sidebar-collapsed'); $('#theme-toggle').onclick=()=>{document.body.classList.toggle('light');$('#theme-toggle').innerHTML=i(document.body.classList.contains('light')?'moon':'sun');icons();}; $('#thinking-toggle').onclick=()=>{settings.thinking=!settings.thinking;storeSettings();};$('#tools-toggle').onclick=()=>{settings.tools=!settings.tools;storeSettings();}; $('#attach').onclick=()=>$('#attachment').click();$('#attachment').onchange=async()=>{const f=$('#attachment').files[0];if(!f)return;if(f.size>2e6)return toast('Choose a text file smaller than 2 MB.');attachment={name:f.name,text:await f.text()};$('#attachment-preview').textContent=`Attached: ${f.name} · click to remove`;$('#attachment-preview').hidden=false;$('#attachment').value='';};$('#attachment-preview').onclick=()=>{attachment=null;$('#attachment-preview').hidden=true;}; document.addEventListener('keydown',ev=>{if((ev.metaKey||ev.ctrlKey)&&ev.key==='k'){ev.preventDefault();newChat();}if(ev.key==='Escape')toggleSettings(false);}); wireLaunch({getSettings:()=>settings,updateSettings:patch=>{settings={...settings,...patch};storeSettings();},engine,openSettings:()=>toggleSettings(true),showDevice:async()=>{const g=await engine.probe();$('#info-content').innerHTML=`DEVICE READINESS

${g.ok?'Ready for liftoff.':'WebGPU unavailable.'}

${e(g.ok?g.name:g.message)}

${g.ok?'WebGPU':'Unavailable'}inference backend
${formatBytes(gpuMemoryBytes(settings))}estimated GPU memory

Weights download from Hugging Face. Prompts and files stay in your browser. ${settings.saveModel?'Model storage is enabled.':'Model storage is off; weights are temporary for this session.'}

`;$('#info-dialog').showModal();}});syncToggles(); engine.probe().then(g=>{$('#device-name').textContent=g.ok?g.name:'WebGPU unavailable';$('#device-status').textContent=g.ok?'WebGPU available':g.message;$('#device-led').classList.toggle('ready',g.ok);if(!g.ok){$('#load-error').textContent=g.message;$('#load-error').hidden=false;$('#load-model').disabled=true;}}).catch(err=>{$('#device-status').textContent=err.message;}); loadSessions().then(s=>{sessions=s.map(recoverInterruptedSession);const interrupted=sessions.find(item=>item.interrupted);if(interrupted){session=hydrate(structuredClone(interrupted));const notice=document.createElement('div');notice.className='recovery-notice';notice.textContent='An unfinished reply was recovered. Load the model to view its saved draft and retry.';$('#ml-error').prepend(notice);}renderHistory();}).catch(()=>{}); document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='hidden'&&busy)checkpoint(true);}); window.addEventListener('pagehide',()=>{if(busy)checkpoint(true);}); // A narrow, local development harness. This branch is removed from production builds. if(import.meta.env.DEV){window.__minicpm={engine,settings,createArtifact,openArtifact,session,renderMessages};if(new URLSearchParams(location.search).has('test'))import('./selftest.js').then(m=>m.runSelfTest({engine,settings,createArtifact,openArtifact,pauseScene:()=>scene.setMode('paused')}));}