3morixd commited on
Commit
ac53e27
·
verified ·
1 Parent(s): 7ffa44e

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. README.md +20 -5
  2. index.html +364 -18
README.md CHANGED
@@ -1,10 +1,25 @@
1
  ---
2
- title: Browser Chat
3
- emoji: 🐨
4
  colorFrom: blue
5
- colorTo: red
6
  sdk: static
7
- pinned: false
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: dispatchAI Browser Chat
3
+ emoji:
4
  colorFrom: blue
5
+ colorTo: indigo
6
  sdk: static
7
+ app_file: index.html
8
+ pinned: true
9
+ tags:
10
+ - webgpu
11
+ - on-device
12
+ - mobile-ai
13
+ - transformers-js
14
  ---
15
 
16
+ # dispatchAI Browser Chat WebGPU On-Device Inference
17
+
18
+ A **100% browser-based** AI chat running SmolLM2-360M entirely on the user's GPU via WebGPU.
19
+
20
+ - **Zero backend** — static HTML/JS, infinitely scalable, costs nothing ever
21
+ - **Zero data leaves the device** — pure client-side inference
22
+ - **WebGPU accelerated** — falls back to WASM on unsupported browsers
23
+ - **~200MB model** — downloads once, cached by the browser
24
+
25
+ This is the purest expression of dispatchAI's mission: AI that runs on your device, no cloud required.
index.html CHANGED
@@ -1,19 +1,365 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
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 inference
217
+ 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 support
235
+ 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 generation
317
+ 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 response
331
+ 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 load
362
+ initModel();
363
+ </script>
364
+ </body>
365
  </html>