browser-chat / index.html
3morixd's picture
Upload folder using huggingface_hub
ac53e27 verified
Raw
History Blame Contribute Delete
12.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dispatchAI — Mobile AI in Your Browser</title>
<style>
:root {
--ink: #0A0F1A;
--off-white: #F5F7FA;
--electric-blue: #2E6BFF;
--cyan: #1FE0E6;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
background: var(--ink);
color: var(--off-white);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
font-size: 2.5em;
font-weight: 800;
background: linear-gradient(135deg, var(--electric-blue), var(--cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -1px;
}
.tagline {
color: #8892a6;
font-size: 0.9em;
margin-top: 5px;
}
.badge {
display: inline-block;
background: rgba(46, 107, 255, 0.15);
border: 1px solid var(--electric-blue);
color: var(--electric-blue);
padding: 3px 10px;
border-radius: 12px;
font-size: 0.75em;
margin-top: 8px;
}
.chat-container {
width: 100%;
max-width: 700px;
background: #111827;
border: 1px solid #1a2332;
border-radius: 16px;
overflow: hidden;
}
.chat-header {
background: linear-gradient(135deg, rgba(46, 107, 255, 0.1), rgba(31, 224, 230, 0.1));
padding: 15px 20px;
border-bottom: 1px solid #1a2332;
display: flex;
justify-content: space-between;
align-items: center;
}
.model-name {
font-size: 0.85em;
color: var(--cyan);
}
.status {
font-size: 0.75em;
padding: 4px 10px;
border-radius: 8px;
background: #1a2332;
color: #8892a6;
}
.status.loaded { background: rgba(31, 224, 230, 0.15); color: var(--cyan); }
.status.loading { background: rgba(46, 107, 255, 0.15); color: var(--electric-blue); }
.status.error { background: rgba(255, 80, 80, 0.15); color: #ff5050; }
.messages {
height: 400px;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.messages::-webkit-scrollbar { width: 6px; }
.messages::-webkit-scrollbar-thumb { background: #1a2332; border-radius: 3px; }
.msg {
max-width: 85%;
padding: 10px 14px;
border-radius: 12px;
font-size: 0.9em;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.msg.user {
background: var(--electric-blue);
color: white;
align-self: flex-end;
}
.msg.ai {
background: #1a2332;
color: var(--off-white);
align-self: flex-start;
border: 1px solid #233;
}
.msg.system {
background: transparent;
color: #555;
font-size: 0.8em;
align-self: center;
font-style: italic;
}
.input-area {
padding: 15px 20px;
border-top: 1px solid #1a2332;
display: flex;
gap: 10px;
}
.input-area input {
flex: 1;
background: #0d1421;
border: 1px solid #1a2332;
color: var(--off-white);
padding: 12px 16px;
border-radius: 10px;
font-family: inherit;
font-size: 0.9em;
outline: none;
transition: border-color 0.2s;
}
.input-area input:focus { border-color: var(--electric-blue); }
.input-area input:disabled { opacity: 0.5; }
.input-area button {
background: linear-gradient(135deg, var(--electric-blue), var(--cyan));
color: var(--ink);
border: none;
padding: 12px 24px;
border-radius: 10px;
font-family: inherit;
font-weight: 700;
font-size: 0.9em;
cursor: pointer;
transition: opacity 0.2s;
}
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
.stats {
width: 100%;
max-width: 700px;
margin-top: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.stat {
background: #111827;
border: 1px solid #1a2332;
border-radius: 8px;
padding: 8px 14px;
font-size: 0.75em;
color: #8892a6;
}
.stat span { color: var(--cyan); font-weight: 700; }
.footer {
margin-top: 30px;
text-align: center;
color: #555;
font-size: 0.75em;
}
.footer a { color: var(--electric-blue); text-decoration: none; }
</style>
</head>
<body>
<div class="header">
<div class="logo">dispatchAI</div>
<div class="tagline">Mobile AI running entirely in your browser. No server. No cloud. Zero cost.</div>
<div class="badge">100% on-device | WebGPU powered</div>
</div>
<div class="chat-container">
<div class="chat-header">
<span class="model-name" id="modelName">SmolLM2-360M-Instruct</span>
<span class="status" id="status">Click Initialize</span>
</div>
<div class="messages" id="messages">
<div class="msg system">This model runs entirely in your browser using WebGPU. No data leaves your device.</div>
</div>
<div class="input-area">
<input type="text" id="input" placeholder="Ask anything..." disabled autocomplete="off">
<button id="sendBtn" disabled>Send</button>
</div>
</div>
<div class="stats">
<div class="stat">Model: <span id="statModel">SmolLM2-360M</span></div>
<div class="stat">Size: <span>~200MB</span></div>
<div class="stat">Backend: <span id="statBackend">WebGPU</span></div>
<div class="stat">Tokens/s: <span id="statTps"></span></div>
<div class="stat">Load time: <span id="statLoad"></span></div>
</div>
<div class="footer">
Powered by <a href="https://huggingface.co/dispatchAI" target="_blank">dispatchAI</a> |
Model: <a href="https://huggingface.co/dispatchAI/SmolLM2-360M-Instruct-mobile" target="_blank">SmolLM2-360M-Instruct-mobile</a> |
Built with <a href="https://github.com/xenova/transformers.js" target="_blank">transformers.js</a>
</div>
<script type="module">
import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.0';
// Configure for browser inference
env.allowLocalModels = false;
env.useBrowserCache = true;
let generator = null;
let isGenerating = false;
const statusEl = document.getElementById('status');
const modelNameEl = document.getElementById('modelName');
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('sendBtn');
const statTps = document.getElementById('statTps');
const statLoad = document.getElementById('statLoad');
const statBackend = document.getElementById('statBackend');
const MODEL_ID = 'onnx-community/SmolLM2-360M-Instruct-ONNX';
// Check WebGPU support
async function checkWebGPU() {
if (!navigator.gpu) {
statBackend.textContent = 'WASM (no WebGPU)';
return 'wasm';
}
try {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
statBackend.textContent = 'WebGPU';
return 'webgpu';
}
} catch (e) {}
statBackend.textContent = 'WASM fallback';
return 'wasm';
}
function addMessage(text, role) {
const div = document.createElement('div');
div.className = `msg ${role}`;
div.textContent = text;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
return div;
}
async function initModel() {
statusEl.textContent = 'Initializing...';
statusEl.className = 'status loading';
const backend = await checkWebGPU();
const device = backend === 'webgpu' ? 'webgpu' : 'wasm';
try {
const t0 = performance.now();
statusEl.textContent = 'Downloading model...';
generator = await pipeline('text-generation', MODEL_ID, {
device: device,
dtype: backend === 'webgpu' ? 'q4f16' : 'q8',
});
const loadTime = ((performance.now() - t0) / 1000).toFixed(1);
statLoad.textContent = `${loadTime}s`;
statusEl.textContent = 'Ready';
statusEl.className = 'status loaded';
inputEl.disabled = false;
sendBtn.disabled = false;
inputEl.focus();
addMessage(`Model loaded in ${loadTime}s on ${backend.toUpperCase()}. Ask me anything!`, 'system');
} catch (e) {
statusEl.textContent = 'Failed to load';
statusEl.className = 'status error';
addMessage(`Error: ${e.message}. Try refreshing the page.`, 'system');
console.error(e);
}
}
async function generate() {
if (isGenerating || !generator) return;
const text = inputEl.value.trim();
if (!text) return;
inputEl.value = '';
inputEl.disabled = true;
sendBtn.disabled = true;
sendBtn.textContent = '...';
isGenerating = true;
addMessage(text, 'user');
const aiMsg = addMessage('', 'ai');
const messages = [
{ role: 'system', content: 'You are a helpful assistant. Keep responses concise.' },
{ role: 'user', content: text },
];
try {
const t0 = performance.now();
let tokenCount = 0;
// Streaming generation
const stream = await generator(messages, {
max_new_tokens: 128,
do_sample: true,
temperature: 0.7,
streaming: true,
callback_function: (data) => {
tokenCount++;
},
});
const elapsed = (performance.now() - t0) / 1000;
const output = stream[0].generated_text[2].content;
// Type out the response
let i = 0;
const typeInterval = setInterval(() => {
if (i < output.length) {
aiMsg.textContent = output.slice(0, i + 1);
messagesEl.scrollTop = messagesEl.scrollHeight;
i++;
} else {
clearInterval(typeInterval);
const tps = (tokenCount / elapsed).toFixed(1);
statTps.textContent = `${tps}`;
}
}, 10);
} catch (e) {
aiMsg.textContent = `Error: ${e.message}`;
console.error(e);
} finally {
inputEl.disabled = false;
sendBtn.disabled = false;
sendBtn.textContent = 'Send';
isGenerating = false;
inputEl.focus();
}
}
sendBtn.addEventListener('click', generate);
inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') generate();
});
// Auto-init on page load
initModel();
</script>
</body>
</html>