CoolFace
Apppublic

dispatchAI/browser-chat

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
index.html366 linesDownload Raw Back to root
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