SiddhJagani commited on
Commit
d899bb1
·
verified ·
1 Parent(s): f72edcb

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +859 -19
index.html CHANGED
@@ -1,19 +1,859 @@
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>LOCAL MIND — On-Device AI</title>
7
+ <script type="module">
8
+ import { CreateMLCEngine } from "https://esm.run/@mlc-ai/web-llm";
9
+
10
+ // ─── CONFIG ───────────────────────────────────────────────────────────────
11
+ const MODEL_ID = "Qwen3-0.6B-q4f16_1-MLC";
12
+ let engine = null;
13
+ let isLoaded = false;
14
+ let chatHistory = [
15
+ {
16
+ role: 'system',
17
+ content: 'You are a helpful, friendly, and knowledgeable assistant. Answer questions clearly and concisely. Be direct and accurate. Do not include excessive preamble or repeat the question back. Respond naturally in the language the user uses.'
18
+ }
19
+ ];
20
+
21
+ const $ = id => document.getElementById(id);
22
+ const statusEl = $('status');
23
+ const progressEl = $('progress');
24
+ const progressBar = $('progress-bar');
25
+ const progressText = $('progress-text');
26
+ const chatContainer = $('chat-container');
27
+ const inputEl = $('user-input');
28
+ const sendBtn = $('send-btn');
29
+ const loadBtn = $('load-btn');
30
+ const storageInfo = $('storage-info');
31
+ const cacheIndicator = $('cache-indicator');
32
+
33
+ // ─── CHECK CACHE ──────────────────────────────────────────────────────────
34
+ async function checkCache() {
35
+ try {
36
+ const mlcCache = await caches.open('webllm/model');
37
+ const keys = await mlcCache.keys();
38
+ const modelCached = keys.some(r => r.url.includes('Qwen3-0.6B'));
39
+ if (modelCached) {
40
+ cacheIndicator.innerHTML = `<span class="dot cached"></span> Model cached locally`;
41
+ cacheIndicator.classList.add('has-cache');
42
+ loadBtn.textContent = 'Load (From Cache)';
43
+ } else {
44
+ cacheIndicator.innerHTML = `<span class="dot"></span> Not cached — will download once`;
45
+ }
46
+ if ('storage' in navigator && 'estimate' in navigator.storage) {
47
+ const est = await navigator.storage.estimate();
48
+ const usedMB = ((est.usage || 0) / 1024 / 1024).toFixed(0);
49
+ const quotaGB = ((est.quota || 0) / 1024 / 1024 / 1024).toFixed(1);
50
+ storageInfo.textContent = `Browser storage: ${usedMB}MB used / ${quotaGB}GB available`;
51
+ }
52
+ } catch(e) {
53
+ cacheIndicator.innerHTML = `<span class="dot"></span> Cache status unknown`;
54
+ }
55
+ }
56
+
57
+ // ─── LOAD MODEL ───────────────────────────────────────────────────────────
58
+ async function loadModel() {
59
+ loadBtn.disabled = true;
60
+ loadBtn.textContent = 'Loading...';
61
+ progressEl.style.display = 'flex';
62
+ statusEl.textContent = 'Initializing WebGPU engine...';
63
+ $('welcome').style.display = 'none';
64
+
65
+ try {
66
+ engine = await CreateMLCEngine(MODEL_ID, {
67
+ initProgressCallback: (report) => {
68
+ const pct = Math.round((report.progress || 0) * 100);
69
+ progressBar.style.width = `${pct}%`;
70
+ const msg = (report.text || `${pct}%`).substring(0, 60);
71
+ progressText.textContent = msg;
72
+
73
+ if (report.progress >= 1) {
74
+ statusEl.textContent = 'Model ready — running fully on your device';
75
+ progressEl.style.display = 'none';
76
+ isLoaded = true;
77
+ inputEl.disabled = false;
78
+ sendBtn.disabled = false;
79
+ inputEl.placeholder = 'Ask anything...';
80
+ loadBtn.style.display = 'none';
81
+ checkCache();
82
+ addSystemMessage('✓ Model loaded & cached. Everything runs on-device. Zero data leaves your browser.');
83
+ }
84
+ }
85
+ });
86
+ } catch(err) {
87
+ statusEl.textContent = `Error: ${err.message}`;
88
+ progressEl.style.display = 'none';
89
+ loadBtn.disabled = false;
90
+ loadBtn.textContent = 'Retry Load';
91
+ if (err.message && err.message.includes('WebGPU')) {
92
+ addSystemMessage('⚠️ WebGPU not supported. Use Chrome 113+ or Edge 113+.');
93
+ } else {
94
+ addSystemMessage(`Error loading model: ${err.message}`);
95
+ }
96
+ }
97
+ }
98
+
99
+ // ─── MESSAGES ─────────────────────────────────────────────────────────────
100
+ function addSystemMessage(text) {
101
+ const div = document.createElement('div');
102
+ div.className = 'msg system-msg';
103
+ div.textContent = text;
104
+ chatContainer.appendChild(div);
105
+ chatContainer.scrollTop = chatContainer.scrollHeight;
106
+ }
107
+
108
+ function addMessage(role, content) {
109
+ const div = document.createElement('div');
110
+ div.className = `msg ${role}-msg`;
111
+
112
+ const label = document.createElement('span');
113
+ label.className = 'msg-label';
114
+ label.textContent = role === 'user' ? 'YOU' : 'AI';
115
+
116
+ const contentWrapper = document.createElement('div');
117
+ contentWrapper.className = 'msg-content-wrapper';
118
+
119
+ // Thinking animation (shown while inside <think> block)
120
+ const thinkingEl = document.createElement('div');
121
+ thinkingEl.className = 'thinking-indicator';
122
+ // Label next to dots
123
+ const thinkingLabel = document.createElement('span');
124
+ thinkingLabel.className = 'thinking-label';
125
+ thinkingLabel.textContent = 'Reasoning';
126
+ const dotsWrap = document.createElement('div');
127
+ dotsWrap.className = 'thinking-dots';
128
+ for (let i = 0; i < 3; i++) {
129
+ const dot = document.createElement('span');
130
+ dot.className = 'thinking-dot';
131
+ dotsWrap.appendChild(dot);
132
+ }
133
+ thinkingEl.appendChild(thinkingLabel);
134
+ thinkingEl.appendChild(dotsWrap);
135
+ thinkingEl.style.display = role === 'assistant' ? 'flex' : 'none';
136
+
137
+ // Main response text
138
+ const text = document.createElement('p');
139
+ text.className = 'msg-text';
140
+ text.textContent = content;
141
+
142
+ // Reasoning dropdown (hidden until thinking is done)
143
+ const reasoningToggle = document.createElement('button');
144
+ reasoningToggle.className = 'reasoning-toggle';
145
+ reasoningToggle.innerHTML = '▶ Reasoning';
146
+ reasoningToggle.style.display = 'none';
147
+
148
+ const reasoningContent = document.createElement('div');
149
+ reasoningContent.className = 'reasoning-content';
150
+ reasoningContent.style.display = 'none';
151
+
152
+ reasoningToggle.addEventListener('click', () => {
153
+ const isHidden = reasoningContent.style.display === 'none';
154
+ reasoningContent.style.display = isHidden ? 'block' : 'none';
155
+ reasoningToggle.innerHTML = isHidden ? '▼ Reasoning' : '▶ Reasoning';
156
+ });
157
+
158
+ contentWrapper.appendChild(thinkingEl);
159
+ contentWrapper.appendChild(text);
160
+ contentWrapper.appendChild(reasoningToggle);
161
+ contentWrapper.appendChild(reasoningContent);
162
+ div.appendChild(label);
163
+ div.appendChild(contentWrapper);
164
+ chatContainer.appendChild(div);
165
+ chatContainer.scrollTop = chatContainer.scrollHeight;
166
+
167
+ return { text, thinkingEl, reasoningToggle, reasoningContent };
168
+ }
169
+
170
+ // ─── PARSE RESPONSE ───────────────────────────────────────────────────────
171
+ // Qwen3 thinking models output: <think>...reasoning...</think> actual response
172
+ // We parse this in real-time as tokens stream in.
173
+ function parseResponse(fullText, elements) {
174
+ const { reasoningContent, reasoningToggle, thinkingEl } = elements || {};
175
+
176
+ const THINK_OPEN = '<think>';
177
+ const THINK_CLOSE = '</think>';
178
+
179
+ const startIdx = fullText.indexOf(THINK_OPEN);
180
+
181
+ // No <think> tag found at all — plain response
182
+ if (startIdx === -1) {
183
+ if (thinkingEl) thinkingEl.style.display = 'none';
184
+ return fullText.trim();
185
+ }
186
+
187
+ const endIdx = fullText.indexOf(THINK_CLOSE);
188
+
189
+ if (endIdx === -1) {
190
+ // Still streaming inside <think>...</think> — show animation, populate reasoning
191
+ if (thinkingEl) thinkingEl.style.display = 'flex';
192
+ const reasoning = fullText.substring(startIdx + THINK_OPEN.length);
193
+ if (reasoningContent) reasoningContent.textContent = reasoning;
194
+ // Don't show toggle yet — we're still thinking
195
+ if (reasoningToggle) reasoningToggle.style.display = 'none';
196
+ return ''; // No visible response yet
197
+ }
198
+
199
+ // </think> found — thinking is complete, extract both parts
200
+ if (thinkingEl) thinkingEl.style.display = 'none';
201
+
202
+ const reasoning = fullText.substring(startIdx + THINK_OPEN.length, endIdx).trim();
203
+ const response = fullText.substring(endIdx + THINK_CLOSE.length).trim();
204
+
205
+ if (reasoningContent) reasoningContent.textContent = reasoning;
206
+ if (reasoningToggle && reasoning.length > 0) {
207
+ reasoningToggle.style.display = 'inline-flex';
208
+ }
209
+
210
+ return response;
211
+ }
212
+
213
+ // ─── SEND ─────────────────────────────────────────────────────────────────
214
+ async function sendMessage() {
215
+ if (!isLoaded || !engine) return;
216
+ const userText = inputEl.value.trim();
217
+ if (!userText) return;
218
+
219
+ inputEl.value = '';
220
+ inputEl.style.height = 'auto';
221
+ sendBtn.disabled = true;
222
+ inputEl.disabled = true;
223
+
224
+ addMessage('user', userText);
225
+ chatHistory.push({ role: 'user', content: userText });
226
+
227
+ const aiElements = addMessage('assistant', '');
228
+ const { text, thinkingEl, reasoningToggle, reasoningContent } = aiElements;
229
+
230
+ try {
231
+ const stream = await engine.chat.completions.create({
232
+ messages: chatHistory,
233
+ stream: true,
234
+ temperature: 0.7,
235
+ max_tokens: 1024,
236
+ });
237
+
238
+ let fullResponse = '';
239
+
240
+ for await (const chunk of stream) {
241
+ const delta = chunk.choices[0]?.delta?.content || '';
242
+ fullResponse += delta;
243
+
244
+ const displayText = parseResponse(fullResponse, {
245
+ reasoningContent,
246
+ reasoningToggle,
247
+ thinkingEl
248
+ });
249
+
250
+ text.textContent = displayText;
251
+ chatContainer.scrollTop = chatContainer.scrollHeight;
252
+ }
253
+
254
+ // Final parse pass
255
+ const finalText = parseResponse(fullResponse, {
256
+ reasoningContent,
257
+ reasoningToggle,
258
+ thinkingEl
259
+ });
260
+ text.textContent = finalText;
261
+
262
+ // Ensure thinking anim is hidden
263
+ thinkingEl.style.display = 'none';
264
+
265
+ // Store only the actual response (no <think> tags) in history
266
+ chatHistory.push({ role: 'assistant', content: finalText });
267
+
268
+ } catch(err) {
269
+ thinkingEl.style.display = 'none';
270
+ text.textContent = `Error: ${err.message}`;
271
+ }
272
+
273
+ sendBtn.disabled = false;
274
+ inputEl.disabled = false;
275
+ inputEl.focus();
276
+ }
277
+
278
+ // ─── EVENTS ───────────────────────────────────────────────────────────────
279
+ loadBtn.addEventListener('click', loadModel);
280
+ sendBtn.addEventListener('click', sendMessage);
281
+
282
+ inputEl.addEventListener('keydown', e => {
283
+ if (e.key === 'Enter' && !e.shiftKey) {
284
+ e.preventDefault();
285
+ sendMessage();
286
+ }
287
+ });
288
+
289
+ $('clear-btn').addEventListener('click', () => {
290
+ chatHistory = [
291
+ {
292
+ role: 'system',
293
+ content: 'You are a helpful, friendly, and knowledgeable assistant. Answer questions clearly and concisely. Be direct and accurate. Do not include excessive preamble or repeat the question back. Respond naturally in the language the user uses.'
294
+ }
295
+ ];
296
+ chatContainer.innerHTML = '';
297
+ addSystemMessage('Conversation cleared. Model still loaded.');
298
+ });
299
+
300
+ // ─── INIT ─────────────────────────────────────────────────────────────────
301
+ window.addEventListener('load', () => {
302
+ checkCache();
303
+ if (!navigator.gpu) {
304
+ statusEl.textContent = '⚠️ WebGPU required — use Chrome 113+ / Edge 113+';
305
+ loadBtn.disabled = true;
306
+ loadBtn.title = 'WebGPU not available in this browser';
307
+ }
308
+ });
309
+ </script>
310
+ <style>
311
+ @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Bebas+Neue&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap');
312
+
313
+ :root {
314
+ --bg: #080808;
315
+ --surface: #0f0f0f;
316
+ --surface2: #141414;
317
+ --border: #1e1e1e;
318
+ --border2: #2a2a2a;
319
+ --accent: #c8ff00;
320
+ --accent2: #00ffc8;
321
+ --text: #ddd;
322
+ --muted: #444;
323
+ --ai-border: #2a3a0a;
324
+ --ai-bg: #0d1205;
325
+ }
326
+
327
+ * { box-sizing: border-box; margin: 0; padding: 0; }
328
+
329
+ body {
330
+ background: var(--bg);
331
+ color: var(--text);
332
+ font-family: 'DM Sans', sans-serif;
333
+ font-weight: 300;
334
+ height: 100dvh;
335
+ display: flex;
336
+ flex-direction: column;
337
+ overflow: hidden;
338
+ }
339
+
340
+ /* HEADER */
341
+ header {
342
+ padding: 14px 20px;
343
+ border-bottom: 1px solid var(--border);
344
+ display: flex;
345
+ align-items: center;
346
+ justify-content: space-between;
347
+ background: var(--surface);
348
+ flex-shrink: 0;
349
+ }
350
+
351
+ .logo { display: flex; align-items: baseline; gap: 10px; }
352
+
353
+ .logo-text {
354
+ font-family: 'Bebas Neue', sans-serif;
355
+ font-size: 26px;
356
+ letter-spacing: 4px;
357
+ color: var(--accent);
358
+ line-height: 1;
359
+ }
360
+
361
+ .logo-badge {
362
+ font-family: 'DM Mono', monospace;
363
+ font-size: 9px;
364
+ color: #000;
365
+ background: var(--accent);
366
+ padding: 2px 6px;
367
+ letter-spacing: 1px;
368
+ }
369
+
370
+ .header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
371
+
372
+ #cache-indicator {
373
+ font-family: 'DM Mono', monospace;
374
+ font-size: 10px;
375
+ color: var(--muted);
376
+ display: flex;
377
+ align-items: center;
378
+ gap: 6px;
379
+ transition: color 0.4s;
380
+ }
381
+
382
+ #cache-indicator.has-cache { color: var(--accent); }
383
+
384
+ .dot {
385
+ width: 5px; height: 5px; border-radius: 50%;
386
+ background: var(--muted); display: inline-block;
387
+ animation: blink 2s infinite;
388
+ }
389
+ .dot.cached { background: var(--accent); animation: none; }
390
+
391
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
392
+
393
+ #storage-info {
394
+ font-family: 'DM Mono', monospace;
395
+ font-size: 9px;
396
+ color: #2a2a2a;
397
+ }
398
+
399
+ /* STATUS BAR */
400
+ .status-bar {
401
+ padding: 7px 20px;
402
+ background: var(--surface2);
403
+ border-bottom: 1px solid var(--border);
404
+ display: flex;
405
+ align-items: center;
406
+ gap: 14px;
407
+ flex-shrink: 0;
408
+ min-height: 40px;
409
+ }
410
+
411
+ #status {
412
+ font-family: 'DM Mono', monospace;
413
+ font-size: 10px;
414
+ color: var(--muted);
415
+ flex: 1;
416
+ white-space: nowrap;
417
+ overflow: hidden;
418
+ text-overflow: ellipsis;
419
+ }
420
+
421
+ #progress { display: none; align-items: center; gap: 10px; width: 260px; flex-shrink: 0; }
422
+
423
+ .progress-track {
424
+ flex: 1; height: 2px;
425
+ background: var(--border2);
426
+ border-radius: 1px; overflow: hidden;
427
+ }
428
+
429
+ #progress-bar {
430
+ height: 100%; background: var(--accent); width: 0%;
431
+ transition: width 0.4s ease;
432
+ box-shadow: 0 0 6px var(--accent);
433
+ }
434
+
435
+ #progress-text {
436
+ font-family: 'DM Mono', monospace;
437
+ font-size: 9px;
438
+ color: var(--accent);
439
+ width: 50px; text-align: right;
440
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
441
+ }
442
+
443
+ .model-tag {
444
+ font-family: 'DM Mono', monospace;
445
+ font-size: 9px;
446
+ color: #2e2e2e;
447
+ border: 1px solid #1a1a1a;
448
+ padding: 3px 8px;
449
+ flex-shrink: 0;
450
+ }
451
+
452
+ #load-btn {
453
+ font-family: 'Bebas Neue', sans-serif;
454
+ font-size: 14px;
455
+ letter-spacing: 2px;
456
+ background: var(--accent);
457
+ color: #000;
458
+ border: none;
459
+ padding: 7px 18px;
460
+ cursor: pointer;
461
+ transition: all 0.15s;
462
+ flex-shrink: 0;
463
+ }
464
+
465
+ #load-btn:hover:not(:disabled) {
466
+ background: var(--accent2);
467
+ box-shadow: 0 0 18px rgba(200,255,0,0.25);
468
+ }
469
+
470
+ #load-btn:disabled { opacity: 0.3; cursor: not-allowed; }
471
+
472
+ /* CHAT */
473
+ #chat-container {
474
+ flex: 1;
475
+ overflow-y: auto;
476
+ padding: 20px;
477
+ display: flex;
478
+ flex-direction: column;
479
+ gap: 14px;
480
+ position: relative;
481
+ }
482
+
483
+ #chat-container::-webkit-scrollbar { width: 3px; }
484
+ #chat-container::-webkit-scrollbar-thumb { background: #1e1e1e; }
485
+
486
+ /* WELCOME */
487
+ #welcome {
488
+ position: absolute;
489
+ inset: 0;
490
+ display: flex;
491
+ flex-direction: column;
492
+ align-items: center;
493
+ justify-content: center;
494
+ gap: 24px;
495
+ pointer-events: none;
496
+ user-select: none;
497
+ }
498
+
499
+ .welcome-title {
500
+ font-family: 'Bebas Neue', sans-serif;
501
+ font-size: clamp(48px, 8vw, 80px);
502
+ letter-spacing: 8px;
503
+ color: #141414;
504
+ line-height: 1;
505
+ text-align: center;
506
+ }
507
+
508
+ .welcome-features {
509
+ display: flex;
510
+ gap: 0;
511
+ border: 1px solid #141414;
512
+ }
513
+
514
+ .wf {
515
+ padding: 8px 16px;
516
+ border-right: 1px solid #141414;
517
+ text-align: center;
518
+ }
519
+ .wf:last-child { border-right: none; }
520
+
521
+ .wf-icon { font-size: 16px; margin-bottom: 4px; opacity: 0.3; }
522
+
523
+ .wf-text {
524
+ font-family: 'DM Mono', monospace;
525
+ font-size: 9px;
526
+ color: #1e1e1e;
527
+ letter-spacing: 1px;
528
+ text-transform: uppercase;
529
+ line-height: 1.6;
530
+ }
531
+
532
+ /* MESSAGES */
533
+ .msg { max-width: 680px; animation: fadeUp 0.2s ease; }
534
+
535
+ @keyframes fadeUp {
536
+ from { opacity: 0; transform: translateY(6px); }
537
+ to { opacity: 1; transform: translateY(0); }
538
+ }
539
+
540
+ .system-msg {
541
+ font-family: 'DM Mono', monospace;
542
+ font-size: 10px;
543
+ color: #2a2a2a;
544
+ align-self: center;
545
+ text-align: center;
546
+ max-width: 100%;
547
+ padding: 4px 0;
548
+ }
549
+
550
+ .user-msg {
551
+ align-self: flex-end;
552
+ background: #131313;
553
+ border: 1px solid var(--border2);
554
+ border-radius: 1px 1px 0 1px;
555
+ padding: 12px 16px;
556
+ }
557
+
558
+ .assistant-msg {
559
+ align-self: flex-start;
560
+ background: var(--ai-bg);
561
+ border: 1px solid var(--ai-border);
562
+ border-left: 2px solid var(--accent);
563
+ border-radius: 1px 1px 1px 0;
564
+ padding: 12px 16px;
565
+ }
566
+
567
+ .msg-label {
568
+ font-family: 'DM Mono', monospace;
569
+ font-size: 8px;
570
+ letter-spacing: 2px;
571
+ color: var(--muted);
572
+ display: block;
573
+ margin-bottom: 6px;
574
+ }
575
+
576
+ .assistant-msg .msg-label { color: var(--accent); opacity: 0.5; }
577
+
578
+ .msg p {
579
+ font-size: 13.5px;
580
+ line-height: 1.75;
581
+ font-weight: 300;
582
+ white-space: pre-wrap;
583
+ word-break: break-word;
584
+ }
585
+
586
+ .msg-content-wrapper {
587
+ display: flex;
588
+ flex-direction: column;
589
+ gap: 8px;
590
+ }
591
+
592
+ /* ── THINKING ANIMATION ── */
593
+ .thinking-indicator {
594
+ display: none; /* shown via JS when inside <think> */
595
+ align-items: center;
596
+ gap: 8px;
597
+ padding: 2px 0 4px;
598
+ }
599
+
600
+ .thinking-label {
601
+ font-family: 'DM Mono', monospace;
602
+ font-size: 9px;
603
+ letter-spacing: 1px;
604
+ text-transform: uppercase;
605
+ color: var(--accent);
606
+ opacity: 0.6;
607
+ }
608
+
609
+ .thinking-dots { display: flex; gap: 4px; align-items: center; }
610
+
611
+ .thinking-dot {
612
+ width: 5px;
613
+ height: 5px;
614
+ background: var(--accent);
615
+ border-radius: 50%;
616
+ animation: thinkBounce 1.3s ease-in-out infinite both;
617
+ }
618
+ .thinking-dot:nth-child(1) { animation-delay: 0s; }
619
+ .thinking-dot:nth-child(2) { animation-delay: 0.18s; }
620
+ .thinking-dot:nth-child(3) { animation-delay: 0.36s; }
621
+
622
+ @keyframes thinkBounce {
623
+ 0%, 80%, 100% { transform: scale(0.55); opacity: 0.25; }
624
+ 40% { transform: scale(1); opacity: 1; }
625
+ }
626
+
627
+ /* ── REASONING DROPDOWN ── */
628
+ .reasoning-toggle {
629
+ display: none; /* shown via JS after </think> */
630
+ align-items: center;
631
+ gap: 5px;
632
+ background: transparent;
633
+ border: 1px solid var(--border2);
634
+ color: var(--muted);
635
+ font-family: 'DM Mono', monospace;
636
+ font-size: 9px;
637
+ padding: 4px 10px;
638
+ cursor: pointer;
639
+ border-radius: 2px;
640
+ align-self: flex-start;
641
+ transition: all 0.2s;
642
+ text-transform: uppercase;
643
+ letter-spacing: 1px;
644
+ }
645
+
646
+ .reasoning-toggle:hover {
647
+ border-color: var(--accent);
648
+ color: var(--accent);
649
+ }
650
+
651
+ .reasoning-content {
652
+ margin-top: 2px;
653
+ padding: 10px 12px;
654
+ background: rgba(0,0,0,0.3);
655
+ border: 1px solid var(--border2);
656
+ border-left: 2px solid var(--accent2);
657
+ border-radius: 2px;
658
+ font-size: 11.5px;
659
+ line-height: 1.6;
660
+ color: #666;
661
+ white-space: pre-wrap;
662
+ word-break: break-word;
663
+ font-family: 'DM Mono', monospace;
664
+ }
665
+
666
+ /* INPUT */
667
+ .input-area {
668
+ border-top: 1px solid var(--border);
669
+ padding: 14px 20px;
670
+ background: var(--surface);
671
+ display: flex;
672
+ gap: 10px;
673
+ align-items: flex-end;
674
+ flex-shrink: 0;
675
+ }
676
+
677
+ #user-input {
678
+ flex: 1;
679
+ background: var(--surface2);
680
+ border: 1px solid var(--border2);
681
+ color: var(--text);
682
+ font-family: 'DM Sans', sans-serif;
683
+ font-size: 13.5px;
684
+ font-weight: 300;
685
+ padding: 11px 14px;
686
+ resize: none;
687
+ outline: none;
688
+ transition: border-color 0.2s;
689
+ min-height: 42px;
690
+ max-height: 120px;
691
+ border-radius: 0;
692
+ line-height: 1.5;
693
+ }
694
+
695
+ #user-input:focus { border-color: var(--accent); }
696
+ #user-input:disabled { opacity: 0.25; }
697
+ #user-input::placeholder { color: #2a2a2a; }
698
+
699
+ #send-btn {
700
+ font-family: 'Bebas Neue', sans-serif;
701
+ font-size: 14px;
702
+ letter-spacing: 2px;
703
+ background: transparent;
704
+ color: var(--accent);
705
+ border: 1px solid var(--accent);
706
+ padding: 9px 18px;
707
+ cursor: pointer;
708
+ transition: all 0.15s;
709
+ height: 42px;
710
+ flex-shrink: 0;
711
+ }
712
+
713
+ #send-btn:hover:not(:disabled) {
714
+ background: var(--accent);
715
+ color: #000;
716
+ box-shadow: 0 0 14px rgba(200,255,0,0.15);
717
+ }
718
+
719
+ #send-btn:disabled { opacity: 0.15; cursor: not-allowed; }
720
+
721
+ #clear-btn {
722
+ font-family: 'DM Mono', monospace;
723
+ font-size: 9px;
724
+ letter-spacing: 1px;
725
+ background: transparent;
726
+ color: var(--muted);
727
+ border: 1px solid var(--border2);
728
+ padding: 9px 12px;
729
+ cursor: pointer;
730
+ height: 42px;
731
+ transition: all 0.15s;
732
+ flex-shrink: 0;
733
+ text-transform: uppercase;
734
+ }
735
+
736
+ #clear-btn:hover { border-color: #444; color: #888; }
737
+
738
+ /* SPECS BAR */
739
+ .specs {
740
+ display: flex;
741
+ border-top: 1px solid var(--border);
742
+ flex-shrink: 0;
743
+ overflow-x: auto;
744
+ }
745
+
746
+ .spec-item {
747
+ flex: 1;
748
+ padding: 7px 14px;
749
+ border-right: 1px solid var(--border);
750
+ min-width: 90px;
751
+ }
752
+ .spec-item:last-child { border-right: none; }
753
+
754
+ .spec-label {
755
+ font-family: 'DM Mono', monospace;
756
+ font-size: 8px;
757
+ color: #222;
758
+ letter-spacing: 1px;
759
+ text-transform: uppercase;
760
+ margin-bottom: 2px;
761
+ }
762
+
763
+ .spec-val {
764
+ font-family: 'DM Mono', monospace;
765
+ font-size: 10px;
766
+ color: #333;
767
+ }
768
+
769
+ .spec-val.green { color: #5a8a00; }
770
+ </style>
771
+ </head>
772
+ <body>
773
+
774
+ <header>
775
+ <div class="logo">
776
+ <span class="logo-text">LOCAL MIND</span>
777
+ <span class="logo-badge">OFFLINE</span>
778
+ </div>
779
+ <div class="header-right">
780
+ <div id="cache-indicator"><span class="dot"></span> Checking cache...</div>
781
+ <div id="storage-info"></div>
782
+ </div>
783
+ </header>
784
+
785
+ <div class="status-bar">
786
+ <span id="status">Ready — click Load Model to initialize</span>
787
+ <div id="progress">
788
+ <div class="progress-track"><div id="progress-bar"></div></div>
789
+ <span id="progress-text"></span>
790
+ </div>
791
+ <div class="model-tag">Qwen3-0.6B · q4f16_1 · MLC</div>
792
+ <button id="load-btn">Load Model</button>
793
+ </div>
794
+
795
+ <div id="chat-container">
796
+ <div id="welcome">
797
+ <div class="welcome-title">YOUR BRAIN<br>YOUR DEVICE</div>
798
+ <div class="welcome-features">
799
+ <div class="wf">
800
+ <div class="wf-icon">⚡</div>
801
+ <div class="wf-text">WebGPU<br>Accelerated</div>
802
+ </div>
803
+ <div class="wf">
804
+ <div class="wf-icon">💾</div>
805
+ <div class="wf-text">One-Time<br>Download</div>
806
+ </div>
807
+ <div class="wf">
808
+ <div class="wf-icon">🔒</div>
809
+ <div class="wf-text">Zero Data<br>Leaves Browser</div>
810
+ </div>
811
+ <div class="wf">
812
+ <div class="wf-icon">♾️</div>
813
+ <div class="wf-text">Cached<br>Forever</div>
814
+ </div>
815
+ </div>
816
+ </div>
817
+ </div>
818
+
819
+ <div class="input-area">
820
+ <textarea
821
+ id="user-input"
822
+ placeholder="Load model first..."
823
+ disabled
824
+ rows="1"
825
+ oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,120)+'px'"
826
+ ></textarea>
827
+ <button id="clear-btn">Clear</button>
828
+ <button id="send-btn" disabled>Send</button>
829
+ </div>
830
+
831
+ <div class="specs">
832
+ <div class="spec-item">
833
+ <div class="spec-label">Inference</div>
834
+ <div class="spec-val green">WebGPU</div>
835
+ </div>
836
+ <div class="spec-item">
837
+ <div class="spec-label">Cache</div>
838
+ <div class="spec-val green">Browser Cache API</div>
839
+ </div>
840
+ <div class="spec-item">
841
+ <div class="spec-label">Privacy</div>
842
+ <div class="spec-val green">100% Local</div>
843
+ </div>
844
+ <div class="spec-item">
845
+ <div class="spec-label">Download</div>
846
+ <div class="spec-val">One-Time ~600MB</div>
847
+ </div>
848
+ <div class="spec-item">
849
+ <div class="spec-label">Runtime</div>
850
+ <div class="spec-val">MLC / WebLLM</div>
851
+ </div>
852
+ <div class="spec-item">
853
+ <div class="spec-label">Build</div>
854
+ <div class="spec-val">Zero — Open in Browser</div>
855
+ </div>
856
+ </div>
857
+
858
+ </body>
859
+ </html>