dispatchAI/browser-chat
0
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>dispatchAI — Mobile AI in Your Browser</title>7 <style>8 :root {9 --ink: #0A0F1A;10 --off-white: #F5F7FA;11 --electric-blue: #2E6BFF;12 --cyan: #1FE0E6;13 }14 * { margin: 0; padding: 0; box-sizing: border-box; }15 body {16 font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;17 background: var(--ink);18 color: var(--off-white);19 min-height: 100vh;20 display: flex;21 flex-direction: column;22 align-items: center;23 padding: 20px;24 }25 .header {26 text-align: center;27 margin-bottom: 30px;28 }29 .logo {30 font-size: 2.5em;31 font-weight: 800;32 background: linear-gradient(135deg, var(--electric-blue), var(--cyan));33 -webkit-background-clip: text;34 -webkit-text-fill-color: transparent;35 letter-spacing: -1px;36 }37 .tagline {38 color: #8892a6;39 font-size: 0.9em;40 margin-top: 5px;41 }42 .badge {43 display: inline-block;44 background: rgba(46, 107, 255, 0.15);45 border: 1px solid var(--electric-blue);46 color: var(--electric-blue);47 padding: 3px 10px;48 border-radius: 12px;49 font-size: 0.75em;50 margin-top: 8px;51 }52 .chat-container {53 width: 100%;54 max-width: 700px;55 background: #111827;56 border: 1px solid #1a2332;57 border-radius: 16px;58 overflow: hidden;59 }60 .chat-header {61 background: linear-gradient(135deg, rgba(46, 107, 255, 0.1), rgba(31, 224, 230, 0.1));62 padding: 15px 20px;63 border-bottom: 1px solid #1a2332;64 display: flex;65 justify-content: space-between;66 align-items: center;67 }68 .model-name {69 font-size: 0.85em;70 color: var(--cyan);71 }72 .status {73 font-size: 0.75em;74 padding: 4px 10px;75 border-radius: 8px;76 background: #1a2332;77 color: #8892a6;78 }79 .status.loaded { background: rgba(31, 224, 230, 0.15); color: var(--cyan); }80 .status.loading { background: rgba(46, 107, 255, 0.15); color: var(--electric-blue); }81 .status.error { background: rgba(255, 80, 80, 0.15); color: #ff5050; }82 .messages {83 height: 400px;84 overflow-y: auto;85 padding: 20px;86 display: flex;87 flex-direction: column;88 gap: 12px;89 }90 .messages::-webkit-scrollbar { width: 6px; }91 .messages::-webkit-scrollbar-thumb { background: #1a2332; border-radius: 3px; }92 .msg {93 max-width: 85%;94 padding: 10px 14px;95 border-radius: 12px;96 font-size: 0.9em;97 line-height: 1.5;98 white-space: pre-wrap;99 word-wrap: break-word;100 }101 .msg.user {102 background: var(--electric-blue);103 color: white;104 align-self: flex-end;105 }106 .msg.ai {107 background: #1a2332;108 color: var(--off-white);109 align-self: flex-start;110 border: 1px solid #233;111 }112 .msg.system {113 background: transparent;114 color: #555;115 font-size: 0.8em;116 align-self: center;117 font-style: italic;118 }119 .input-area {120 padding: 15px 20px;121 border-top: 1px solid #1a2332;122 display: flex;123 gap: 10px;124 }125 .input-area input {126 flex: 1;127 background: #0d1421;128 border: 1px solid #1a2332;129 color: var(--off-white);130 padding: 12px 16px;131 border-radius: 10px;132 font-family: inherit;133 font-size: 0.9em;134 outline: none;135 transition: border-color 0.2s;136 }137 .input-area input:focus { border-color: var(--electric-blue); }138 .input-area input:disabled { opacity: 0.5; }139 .input-area button {140 background: linear-gradient(135deg, var(--electric-blue), var(--cyan));141 color: var(--ink);142 border: none;143 padding: 12px 24px;144 border-radius: 10px;145 font-family: inherit;146 font-weight: 700;147 font-size: 0.9em;148 cursor: pointer;149 transition: opacity 0.2s;150 }151 .input-area button:disabled { opacity: 0.5; cursor: not-allowed; }152 .stats {153 width: 100%;154 max-width: 700px;155 margin-top: 15px;156 display: flex;157 gap: 10px;158 flex-wrap: wrap;159 }160 .stat {161 background: #111827;162 border: 1px solid #1a2332;163 border-radius: 8px;164 padding: 8px 14px;165 font-size: 0.75em;166 color: #8892a6;167 }168 .stat span { color: var(--cyan); font-weight: 700; }169 .footer {170 margin-top: 30px;171 text-align: center;172 color: #555;173 font-size: 0.75em;174 }175 .footer a { color: var(--electric-blue); text-decoration: none; }176 </style>177</head>178<body>179 <div class="header">180 <div class="logo">dispatchAI</div>181 <div class="tagline">Mobile AI running entirely in your browser. No server. No cloud. Zero cost.</div>182 <div class="badge">100% on-device | WebGPU powered</div>183 </div>184 185 <div class="chat-container">186 <div class="chat-header">187 <span class="model-name" id="modelName">SmolLM2-360M-Instruct</span>188 <span class="status" id="status">Click Initialize</span>189 </div>190 <div class="messages" id="messages">191 <div class="msg system">This model runs entirely in your browser using WebGPU. No data leaves your device.</div>192 </div>193 <div class="input-area">194 <input type="text" id="input" placeholder="Ask anything..." disabled autocomplete="off">195 <button id="sendBtn" disabled>Send</button>196 </div>197 </div>198 199 <div class="stats">200 <div class="stat">Model: <span id="statModel">SmolLM2-360M</span></div>201 <div class="stat">Size: <span>~200MB</span></div>202 <div class="stat">Backend: <span id="statBackend">WebGPU</span></div>203 <div class="stat">Tokens/s: <span id="statTps">—</span></div>204 <div class="stat">Load time: <span id="statLoad">—</span></div>205 </div>206 207 <div class="footer">208 Powered by <a href="https://huggingface.co/dispatchAI" target="_blank">dispatchAI</a> |209 Model: <a href="https://huggingface.co/dispatchAI/SmolLM2-360M-Instruct-mobile" target="_blank">SmolLM2-360M-Instruct-mobile</a> |210 Built with <a href="https://github.com/xenova/transformers.js" target="_blank">transformers.js</a>211 </div>212 213 <script type="module">214 import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.0';215 216 // Configure for browser inference217 env.allowLocalModels = false;218 env.useBrowserCache = true;219 220 let generator = null;221 let isGenerating = false;222 223 const statusEl = document.getElementById('status');224 const modelNameEl = document.getElementById('modelName');225 const messagesEl = document.getElementById('messages');226 const inputEl = document.getElementById('input');227 const sendBtn = document.getElementById('sendBtn');228 const statTps = document.getElementById('statTps');229 const statLoad = document.getElementById('statLoad');230 const statBackend = document.getElementById('statBackend');231 232 const MODEL_ID = 'onnx-community/SmolLM2-360M-Instruct-ONNX';233 234 // Check WebGPU support235 async function checkWebGPU() {236 if (!navigator.gpu) {237 statBackend.textContent = 'WASM (no WebGPU)';238 return 'wasm';239 }240 try {241 const adapter = await navigator.gpu.requestAdapter();242 if (adapter) {243 statBackend.textContent = 'WebGPU';244 return 'webgpu';245 }246 } catch (e) {}247 statBackend.textContent = 'WASM fallback';248 return 'wasm';249 }250 251 function addMessage(text, role) {252 const div = document.createElement('div');253 div.className = `msg ${role}`;254 div.textContent = text;255 messagesEl.appendChild(div);256 messagesEl.scrollTop = messagesEl.scrollHeight;257 return div;258 }259 260 async function initModel() {261 statusEl.textContent = 'Initializing...';262 statusEl.className = 'status loading';263 264 const backend = await checkWebGPU();265 const device = backend === 'webgpu' ? 'webgpu' : 'wasm';266 267 try {268 const t0 = performance.now();269 statusEl.textContent = 'Downloading model...';270 271 generator = await pipeline('text-generation', MODEL_ID, {272 device: device,273 dtype: backend === 'webgpu' ? 'q4f16' : 'q8',274 });275 276 const loadTime = ((performance.now() - t0) / 1000).toFixed(1);277 statLoad.textContent = `${loadTime}s`;278 statusEl.textContent = 'Ready';279 statusEl.className = 'status loaded';280 inputEl.disabled = false;281 sendBtn.disabled = false;282 inputEl.focus();283 284 addMessage(`Model loaded in ${loadTime}s on ${backend.toUpperCase()}. Ask me anything!`, 'system');285 } catch (e) {286 statusEl.textContent = 'Failed to load';287 statusEl.className = 'status error';288 addMessage(`Error: ${e.message}. Try refreshing the page.`, 'system');289 console.error(e);290 }291 }292 293 async function generate() {294 if (isGenerating || !generator) return;295 const text = inputEl.value.trim();296 if (!text) return;297 298 inputEl.value = '';299 inputEl.disabled = true;300 sendBtn.disabled = true;301 sendBtn.textContent = '...';302 isGenerating = true;303 304 addMessage(text, 'user');305 const aiMsg = addMessage('', 'ai');306 307 const messages = [308 { role: 'system', content: 'You are a helpful assistant. Keep responses concise.' },309 { role: 'user', content: text },310 ];311 312 try {313 const t0 = performance.now();314 let tokenCount = 0;315 316 // Streaming generation317 const stream = await generator(messages, {318 max_new_tokens: 128,319 do_sample: true,320 temperature: 0.7,321 streaming: true,322 callback_function: (data) => {323 tokenCount++;324 },325 });326 327 const elapsed = (performance.now() - t0) / 1000;328 const output = stream[0].generated_text[2].content;329 330 // Type out the response331 let i = 0;332 const typeInterval = setInterval(() => {333 if (i < output.length) {334 aiMsg.textContent = output.slice(0, i + 1);335 messagesEl.scrollTop = messagesEl.scrollHeight;336 i++;337 } else {338 clearInterval(typeInterval);339 const tps = (tokenCount / elapsed).toFixed(1);340 statTps.textContent = `${tps}`;341 }342 }, 10);343 344 } catch (e) {345 aiMsg.textContent = `Error: ${e.message}`;346 console.error(e);347 } finally {348 inputEl.disabled = false;349 sendBtn.disabled = false;350 sendBtn.textContent = 'Send';351 isGenerating = false;352 inputEl.focus();353 }354 }355 356 sendBtn.addEventListener('click', generate);357 inputEl.addEventListener('keydown', (e) => {358 if (e.key === 'Enter') generate();359 });360 361 // Auto-init on page load362 initModel();363 </script>364</body>365</html>366 