Download src/main.js from ProCreations/minicpm5-2b-webgpu: direct link, hf CLI and curl.
- Browser
- Download file 49.7 kB
-
https://huggingface.co/spaces/ProCreations/minicpm5-2b-webgpu/resolve/main/src/main.js
- Command line
-
hf download hf://spaces/ProCreations/minicpm5-2b-webgpu/src/main.js
-
curl -L -o main.js https://huggingface.co/spaces/ProCreations/minicpm5-2b-webgpu/resolve/main/src/main.js
49.7 kB
| 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=>`<i data-lucide="${name}"></i>`; | |
| 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=` | |
| <div class="shell"> | |
| <aside class="sidebar" aria-label="Sidebar"> | |
| <a class="brand" href="https://huggingface.co/spaces/ProCreations/minicpm5-2b-webgpu" target="_blank" rel="noreferrer"><span class="brand-icon">${i('rocket')}</span><span>MiniCPM<span class="brand-five">5</span><small>by ProCreations</small></span></a> | |
| <button class="new-chat" id="new-chat">${i('plus')}New conversation<span>⌘ K</span></button> | |
| <div class="nav-label">WORKSPACE</div> | |
| <button class="nav-item active" id="chat-nav">${i('message-square')}Playground<span class="nav-dot"></span></button> | |
| <button class="nav-item" id="artifacts-nav">${i('folder-open')}Your creations<span id="artifact-count">0</span></button> | |
| <div class="nav-label">CREATE SOMETHING</div> | |
| ${prompts.map(p=>`<button class="nav-item tool-nav ${p.kind}" data-prompt="${p.kind}">${i(p.icon)}${{html:'Website',document:'Document',spreadsheet:'Spreadsheet',pdf:'PDF'}[p.kind]}<span class="nav-ext">${i('arrow-up-right')}</span></button>`).join('')} | |
| <div class="history-head nav-label">RECENT CHATS<span>ON THIS DEVICE</span></div><div id="history-list" class="history-list"><p>Your ideas start here.</p></div> | |
| <div class="sidebar-bottom"><div class="device-card"><span class="device-icon">${i('cpu')}</span><div><strong id="device-name">Checking your device</strong><small id="device-status">Detecting WebGPU</small></div><span class="status-led" id="device-led"></span></div><button class="sidebar-link" id="about">${i('circle-help')}About this playground${i('arrow-up-right')}</button><div class="sidebar-foot"><span>Built for your browser.</span><button class="icon-button" id="theme-toggle" aria-label="Toggle light mode">${i('sun')}</button></div></div> | |
| </aside> | |
| <div class="main-shell"> | |
| <header class="topbar"><div class="topbar-left"><button id="sidebar-toggle" class="icon-button" aria-label="Toggle sidebar">${i('panel-left-close')}</button><span class="breadcrumb">Playground <span>/</span> <strong>MiniCPM5-2B</strong></span><span class="quant-badge">Q8_0</span></div><div class="topbar-right"><span class="private-badge">${i('shield-check')}100% local</span><button class="icon-button" id="settings-toggle" aria-label="Open model settings">${i('sliders-horizontal')}</button></div></header> | |
| <main class="main" id="main"> | |
| <div class="scroll-area" id="scroll-area"> | |
| <section class="welcome" id="welcome"><div class="chat-welcome-heading"><span class="eyebrow">YOUR ENGINE IS READY</span><h1>What will you create?</h1><p>Think it through. Make it real.</p></div> | |
| <div class="hero"><div class="hero-copy"><div class="eyebrow"><span></span>SMALL MODEL. SERIOUS CAPABILITY.</div><h1>A little engine.<br><span>For big ideas.</span></h1><p class="hero-description">Meet MiniCPM5. Your private creative partner<br class="desktop-br"> for thinking, building, and getting things done.</p><div class="model-pills"><span>2B class</span><span>8-bit precision</span><span>Up to 128K context</span></div><div class="launch-control"><button id="load-model" class="primary-button">${i('zap')}Launch model${i('arrow-up-right')}</button><span id="load-caption">2.68 GB · this session only</span></div>${storageChoice('save-model')}</div><div class="hero-art"><div class="scene-halo"></div><div id="launch-scene" aria-label="An electric-blue MiniCPM rocket floating among 42 orbital layer nodes"></div><span class="scene-coordinate coord-one">42 LAYERS <span>↗</span></span><span class="scene-coordinate coord-two"><span class="tiny-led"></span>LOCAL INTELLIGENCE</span><span class="scene-coordinate coord-three">MINI / 005</span></div></div> | |
| <div class="loading-panel" id="loading-panel" hidden><div class="loading-line"><span><span class="status-led pulse"></span><strong id="load-stage">Preparing launch</strong></span><span id="load-progress-text">0%</span></div><div class="progress-track"><div id="load-progress"></div></div><div class="loading-bottom"><span id="load-detail">Your prompts stay on this device.</span><span id="load-elapsed"></span></div><div class="launch-stages"><span id="stage-download">01 / DOWNLOAD</span><span id="stage-compile">02 / PREPARE GPU</span><span id="stage-ready">03 / READY</span></div></div> | |
| <div class="error-banner" id="load-error" role="alert" hidden></div> | |
| <div class="launch-capabilities"><span>${i('code-2')}Websites</span><span>${i('file-text')}Documents</span><span>${i('table-2')}Spreadsheets</span><span>${i('file-type-2')}PDFs</span></div><div class="possibilities-head"><span>Go from <strong>“what if”</strong> to <strong>“here it is.”</strong></span><span>4 TOOLS. ENDLESS POSSIBILITIES.</span></div> | |
| <div class="prompt-grid">${prompts.map(p=>`<button class="prompt-card ${p.kind}" data-prompt="${p.kind}"><span class="prompt-icon">${i(p.icon)}</span><span class="card-arrow">${i('arrow-up-right')}</span><strong>${p.label}</strong><small>${p.desc}</small></button>`).join('')}</div> | |
| <div class="welcome-note">${i('shield-check')}Your conversations and creations stay on your device. No account or API key needed.</div> | |
| </section> | |
| <section id="messages" class="messages" aria-label="Conversation" hidden></section> | |
| </div> | |
| <div class="composer-area"><div id="generation-status" class="generation-status" aria-live="polite"></div><form id="composer" class="composer"><label class="sr-only" for="prompt">Message MiniCPM5</label><textarea id="prompt" rows="2" placeholder="Ask anything, or make something extraordinary…"></textarea><div id="attachment-preview" hidden></div><div class="composer-toolbar"><div class="composer-options"><button type="button" id="attach" class="icon-button" title="Attach a text file" aria-label="Attach text file">${i('paperclip')}</button><input id="attachment" type="file" accept=".txt,.md,.csv,.json,.html,.css,.js,.py" hidden><span class="toolbar-divider"></span><button type="button" id="thinking-toggle" class="option-button">${i('brain')}Think<span class="toggle-dot"></span></button><button type="button" id="tools-toggle" class="option-button on">${i('sparkles')}Create<span class="toggle-dot"></span></button></div><div class="composer-send"><span class="composer-model"><span class="tiny-led" id="model-led"></span>MiniCPM5-2B</span><button type="submit" id="send" class="send-button" aria-label="Send message">${i('arrow-up')}</button></div></div></form><div class="composer-footer"><span id="runtime-status">WebGPU accelerated · entirely in your browser</span><span>AI can make mistakes. Stay curious.</span></div></div> | |
| </main> | |
| </div> | |
| <aside class="settings-panel" id="settings-panel" hidden aria-label="Model settings"><div class="panel-heading"><div><span class="eyebrow">MAKE IT YOURS</span><h2>Engine room</h2></div><button class="icon-button" id="settings-close" aria-label="Close settings">${i('x')}</button></div><div class="settings-scroll" id="settings-content"></div><div class="launch-settings-actions" id="launch-settings-actions"></div></aside> | |
| </div> | |
| <dialog id="artifact-dialog" class="artifact-dialog"><div class="artifact-header"><div class="artifact-title-group"><span id="dialog-icon"></span><div><strong id="dialog-title"></strong><small id="dialog-filename"></small></div></div><div class="artifact-header-actions"><button id="dialog-download" class="secondary-button">${i('download')}Download</button><button id="dialog-close" class="icon-button" aria-label="Close preview">${i('x')}</button></div></div><div class="artifact-tabs"><div><button id="preview-tab" class="tab active">${i('eye')}Preview</button><button id="code-tab" class="tab">${i('code-2')}Source</button></div><div class="preview-controls"><button id="desktop-preview" class="icon-button active" aria-label="Desktop preview">${i('monitor')}</button><button id="mobile-preview" class="icon-button" aria-label="Mobile preview">${i('smartphone')}</button><button id="copy-code" class="icon-button" aria-label="Copy source">${i('copy')}</button></div></div><div id="artifact-preview" class="artifact-preview"></div><pre id="artifact-code" class="artifact-code" hidden><code></code></pre><div class="artifact-foot"><span>${i('shield-check')}Created locally by MiniCPM5</span><span id="artifact-preview-note"></span></div></dialog> | |
| <dialog id="info-dialog" class="info-dialog"><button id="info-close" class="icon-button dialog-close" aria-label="Close dialog">${i('x')}</button><div id="info-content"></div></dialog> | |
| <div id="toast" class="toast" role="status" hidden></div>`; | |
| 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 `<div class="model-storage-choice"><button type="button" class="storage-switch" id="${id}" role="switch" aria-checked="${settings.saveModel}" ><span class="switch-track" aria-hidden="true"><span></span></span>Save model in browser</button><small>${settings.saveModel?`Saves ${formatBytes(modelDownloadBytes(settings))} for faster future launches.`:'Off · downloads each visit, nothing saved.'}</small></div>`;} | |
| 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=>`<button class="history-item ${s.id===session.id?'selected':''}" data-session="${s.id}" title="${e(s.title)}">${i('message-square')}<span>${e(s.title)}</span></button>`).join(''):'<p>Your ideas start here.</p>';icons(); | |
| } | |
| function allArtifacts(){return session.messages.flatMap(m=>m.artifacts||[]);} | |
| function renderArtifactCard(a){ | |
| let mini=''; | |
| if(a.type==='html')mini=`<iframe title="${e(a.title)} thumbnail" tabindex="-1" sandbox="" referrerpolicy="no-referrer" loading="lazy" srcdoc="${e(sandboxHTML(a.args.html))}"></iframe>`; | |
| else if(a.type==='spreadsheet'){const sh=a.args.sheets[0];mini=`<div class="mini-sheet"><table><thead><tr>${sh.columns.slice(0,4).map(c=>`<th>${e(c)}</th>`).join('')}</tr></thead><tbody>${sh.rows.slice(0,3).map(r=>`<tr>${r.slice(0,4).map(c=>`<td>${e(c??'')}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`;} | |
| else mini=`<div class="mini-paper"><small>${a.type==='pdf'?'PDF / FIELD NOTES':'DOCUMENT'}</small><strong>${e(a.title)}</strong><div>${renderMarkdown(a.args.markdown.slice(0,420))}</div></div>`; | |
| return `<div class="artifact-card ${a.type}" data-artifact="${a.id}"><button class="artifact-open" data-view="${a.id}"><span class="artifact-card-icon">${i(artifactIcons[a.type])}</span><span><strong>${e(a.title)}</strong><small>${e(a.filename)} · ${formatBytes(a.blob.size)}</small></span><span class="artifact-card-arrow">${i('arrow-up-right')}</span></button><button class="artifact-mini" data-view="${a.id}" aria-label="Preview ${e(a.title)}">${mini}<span class="mini-open">${i('maximize-2')}Open preview</span></button><div class="artifact-card-actions"><span>${i('check')}Ready to explore</span><button data-view="${a.id}">${i('eye')}Preview</button><button data-download="${a.id}" aria-label="Download ${e(a.filename)}">${i('download')}</button></div></div>`; | |
| } | |
| function splitThinking(m){let content=m.content||'',reasoning=m.reasoning||'';if(content.includes('<think>')){const end=content.indexOf('</think>');const start=content.indexOf('<think>');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('<function');if(functionStart>=0)content=content.slice(0,functionStart);return {content,reasoning};} | |
| function renderMessage(m){ | |
| if(m.role==='user')return `<article class="message user-message" data-message="${m.id}"><div class="user-bubble">${e(m.content)}</div></article>`; | |
| const {content,reasoning}=splitThinking(m); | |
| return `<article class="message assistant-message" data-message="${m.id}"><div class="assistant-avatar">${i('rocket')}</div><div class="assistant-body"><div class="message-label">MiniCPM5 <span>LOCAL</span>${m.generating?'<span class="working-indicator">working<span>•••</span></span>':''}</div>${reasoning?`<details class="reasoning" ${m.generating?'open':''}><summary>${i('brain')}${m.generating?'Thinking it through':'Thought process'}</summary><div>${renderMarkdown(reasoning)}</div></details>`:''}<div class="message-markdown">${renderMarkdown(content)}</div>${m.toolName?`<div class="tool-running">${i('loader-circle')}Creating ${e(m.toolName.replace('create_',''))}…</div>`:''}${(m.artifacts||[]).map(renderArtifactCard).join('')}${m.error?`<div class="message-error">${e(m.error)}</div>`:''}${m.interrupted?`<div class="recovery-actions">${session.interrupted?'<button class="secondary-button" data-retry-reply>Retry interrupted reply</button>':''}<button class="text-button" data-download-draft="${m.id}">Download saved draft</button></div>`:''}${m.stats?`<div class="message-stats"><span>${i('zap')}${m.stats.tps?m.stats.tps.toFixed(1)+' tok/s':'Completed'}</span><span>${m.stats.tokens||0} tokens</span><span>${m.stats.seconds.toFixed(1)}s</span>${m.stats.ttft?`<span>${m.stats.ttft.toFixed(2)}s to first token</span>`:''}${m.stats.finish==='length'?'<span>Response limit reached</span>':''}<button class="icon-button" data-copy="${m.id}" aria-label="Copy response">${i('copy')}</button></div>`:''}</div></article>`; | |
| } | |
| 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(/<function\s+name=["']([^"']+)["']/)?.[1]; | |
| m.toolName=delta.tool_calls?.find(c=>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=`<span class="eyebrow">YOUR WORKSPACE</span><h2>Your creations</h2><p>Files from this conversation, saved on this device.</p>${artifacts.length?artifacts.map(renderArtifactCard).join(''):'<div class="empty-artifacts">'+i('folder-open')+'<strong>Make your first creation</strong><p>Ask MiniCPM5 to build a website, write a document, make a spreadsheet, or design a PDF.</p></div>'}`;$('#info-dialog').showModal();icons();}; | |
| $('#info-close').onclick=()=>$('#info-dialog').close(); | |
| $('#about').onclick=()=>{$('#info-content').innerHTML=`<span class="eyebrow">LITTLE ENGINE. BIG IDEAS.</span><h2>Intelligence, on your terms.</h2><p>This playground runs <a target="_blank" rel="noreferrer" href="https://huggingface.co/openbmb/MiniCPM5-2B">OpenBMB's MiniCPM5-2B</a> directly on your GPU. No inference server, API keys, account, or usage fees.</p><div class="about-grid"><div><strong>2.52B</strong><small>parameters · 42 layers</small></div><div><strong>Q8_0</strong><small>official 8-bit weights</small></div><div><strong>128K</strong><small>native context ceiling</small></div><div><strong>4 tools</strong><small>files created locally</small></div></div><h3>Made to make things</h3><p>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.</p><h3>Your device is the engine</h3><p>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.</p><p>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.</p><div class="about-links"><a target="_blank" rel="noreferrer" href="https://huggingface.co/openbmb/MiniCPM5-2B-GGUF">Official GGUF ${i('arrow-up-right')}</a><a target="_blank" rel="noreferrer" href="https://huggingface.co/${MODEL.weights}">Verified browser shards ${i('arrow-up-right')}</a><a target="_blank" rel="noreferrer" href="https://huggingface.co/openbmb/MiniCPM5-2B-DSpark">DSpark ${i('arrow-up-right')}</a></div>`;$('#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='')=>`<label class="setting"><span>${label}</span><select id="${id}" ${busy||engine.loading?'disabled':''}>${values.map(([v,n])=>`<option value="${v}" ${String(current)===String(v)?'selected':''}>${n}</option>`).join('')}</select>${hint?`<small>${hint}</small>`:''}</label>`; | |
| const range=(id,label,min,max,step,value)=>`<label class="setting range-setting"><span>${label}<output id="${id}-value">${value}</output></span><input id="${id}" type="range" min="${min}" max="${max}" step="${step}" value="${value}"></label>`; | |
| const estimate=gpuMemoryBytes(settings); | |
| $('#settings-content').innerHTML=`<div class="settings-model"><span class="brand-icon">${i('rocket')}</span><div><strong>MiniCPM5-2B</strong><small>Official Q8_0 · 2.68 GB</small></div><span class="quant-badge">8-BIT</span></div>${storageChoice('save-model-setting')}<div class="section-label">MEMORY & CONTEXT</div>${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.')}<div class="memory-estimate"><div><span>Estimated GPU memory</span><strong id="memory-estimate">${formatBytes(estimate)}</strong></div><div class="memory-bar"><span></span><span id="kv-segment" style="flex:${kvBytes(settings.context,settings.cacheK,settings.cacheV)/MODEL.bytes}"></span></div><div class="memory-legend"><span><b></b>Weights</span><span><b></b>KV cache <strong id="kv-estimate">${formatBytes(kvBytes(settings.context,settings.cacheK,settings.cacheV))}</strong></span></div><small>Plus browser / driver overhead. Available VRAM cannot be measured reliably. Larger windows use more memory.</small></div>${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)}<p class="settings-hint">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.</p><button id="apply-settings" class="secondary-button full">${i('rotate-ccw')}${engine.ready?'Apply & reload model':'Apply settings'}</button><details class="advanced-settings"><summary>Generation & instructions</summary><div class="section-label">GENERATION</div>${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)}<p class="settings-hint">0 uses full-vocabulary sampling, which is slower and disables DSpark.</p>${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<settings.context).map(n=>[n,n.toLocaleString()]),settings.maxTokens,'Thinking and tool arguments share this response budget.')}<label class="setting"><span>Seed <small>−1 for random</small></span><input id="seed-setting" type="number" min="-1" max="2147483647" value="${settings.seed}"></label><div class="section-label">MAKE IT PERSONAL</div><label class="setting"><span>Custom instructions</span><textarea id="system-setting" rows="4" maxlength="12000" placeholder="How should MiniCPM5 respond?">${e(settings.system)}</textarea></label><button id="reset-settings" class="text-button">Restore recommended defaults</button></details><div class="section-label">LOCAL STORAGE</div><p class="settings-hint" id="cache-info">Checking browser cache…</p><button id="clear-cache" class="text-button">Remove downloaded model</button><button id="export-chat" class="text-button">Export conversation (.json)</button><details class="diagnostics"><summary>Runtime details</summary><pre id="diagnostics-text">${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))}</pre></details>`; | |
| 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=`<span class="eyebrow">DEVICE READINESS</span><h2>${g.ok?'Ready for liftoff.':'WebGPU unavailable.'}</h2><p>${e(g.ok?g.name:g.message)}</p><div class="about-grid"><div><strong>${g.ok?'WebGPU':'Unavailable'}</strong><small>inference backend</small></div><div><strong>${formatBytes(gpuMemoryBytes(settings))}</strong><small>estimated GPU memory</small></div></div><p>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.'}</p>`;$('#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')}));} | |