Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LOCAL MIND β On-Device AI</title> | |
| <script type="module"> | |
| import { CreateMLCEngine } from "https://esm.run/@mlc-ai/web-llm"; | |
| // βββ CONFIG βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const MODEL_ID = "Qwen3-0.6B-q4f16_1-MLC"; | |
| let engine = null; | |
| let isLoaded = false; | |
| let chatHistory = [ | |
| { | |
| role: 'system', | |
| 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.' | |
| } | |
| ]; | |
| const $ = id => document.getElementById(id); | |
| const statusEl = $('status'); | |
| const progressEl = $('progress'); | |
| const progressBar = $('progress-bar'); | |
| const progressText = $('progress-text'); | |
| const chatContainer = $('chat-container'); | |
| const inputEl = $('user-input'); | |
| const sendBtn = $('send-btn'); | |
| const loadBtn = $('load-btn'); | |
| const storageInfo = $('storage-info'); | |
| const cacheIndicator = $('cache-indicator'); | |
| // βββ CHECK CACHE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function checkCache() { | |
| try { | |
| const mlcCache = await caches.open('webllm/model'); | |
| const keys = await mlcCache.keys(); | |
| const modelCached = keys.some(r => r.url.includes('Qwen3-0.6B')); | |
| if (modelCached) { | |
| cacheIndicator.innerHTML = `<span class="dot cached"></span> Model cached locally`; | |
| cacheIndicator.classList.add('has-cache'); | |
| loadBtn.textContent = 'Load (From Cache)'; | |
| } else { | |
| cacheIndicator.innerHTML = `<span class="dot"></span> Not cached β will download once`; | |
| } | |
| if ('storage' in navigator && 'estimate' in navigator.storage) { | |
| const est = await navigator.storage.estimate(); | |
| const usedMB = ((est.usage || 0) / 1024 / 1024).toFixed(0); | |
| const quotaGB = ((est.quota || 0) / 1024 / 1024 / 1024).toFixed(1); | |
| storageInfo.textContent = `Browser storage: ${usedMB}MB used / ${quotaGB}GB available`; | |
| } | |
| } catch(e) { | |
| cacheIndicator.innerHTML = `<span class="dot"></span> Cache status unknown`; | |
| } | |
| } | |
| // βββ LOAD MODEL βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function loadModel() { | |
| loadBtn.disabled = true; | |
| loadBtn.textContent = 'Loading...'; | |
| progressEl.style.display = 'flex'; | |
| statusEl.textContent = 'Initializing WebGPU engine...'; | |
| $('welcome').style.display = 'none'; | |
| try { | |
| engine = await CreateMLCEngine(MODEL_ID, { | |
| initProgressCallback: (report) => { | |
| const pct = Math.round((report.progress || 0) * 100); | |
| progressBar.style.width = `${pct}%`; | |
| const msg = (report.text || `${pct}%`).substring(0, 60); | |
| progressText.textContent = msg; | |
| if (report.progress >= 1) { | |
| statusEl.textContent = 'Model ready β running fully on your device'; | |
| progressEl.style.display = 'none'; | |
| isLoaded = true; | |
| inputEl.disabled = false; | |
| sendBtn.disabled = false; | |
| inputEl.placeholder = 'Ask anything...'; | |
| loadBtn.style.display = 'none'; | |
| checkCache(); | |
| addSystemMessage('β Model loaded & cached. Everything runs on-device. Zero data leaves your browser.'); | |
| } | |
| } | |
| }); | |
| } catch(err) { | |
| statusEl.textContent = `Error: ${err.message}`; | |
| progressEl.style.display = 'none'; | |
| loadBtn.disabled = false; | |
| loadBtn.textContent = 'Retry Load'; | |
| if (err.message && err.message.includes('WebGPU')) { | |
| addSystemMessage('β οΈ WebGPU not supported. Use Chrome 113+ or Edge 113+.'); | |
| } else { | |
| addSystemMessage(`Error loading model: ${err.message}`); | |
| } | |
| } | |
| } | |
| // βββ MESSAGES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addSystemMessage(text) { | |
| const div = document.createElement('div'); | |
| div.className = 'msg system-msg'; | |
| div.textContent = text; | |
| chatContainer.appendChild(div); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| function addMessage(role, content) { | |
| const div = document.createElement('div'); | |
| div.className = `msg ${role}-msg`; | |
| const label = document.createElement('span'); | |
| label.className = 'msg-label'; | |
| label.textContent = role === 'user' ? 'YOU' : 'AI'; | |
| const contentWrapper = document.createElement('div'); | |
| contentWrapper.className = 'msg-content-wrapper'; | |
| // Thinking animation (shown while inside <think> block) | |
| const thinkingEl = document.createElement('div'); | |
| thinkingEl.className = 'thinking-indicator'; | |
| // Label next to dots | |
| const thinkingLabel = document.createElement('span'); | |
| thinkingLabel.className = 'thinking-label'; | |
| thinkingLabel.textContent = 'Reasoning'; | |
| const dotsWrap = document.createElement('div'); | |
| dotsWrap.className = 'thinking-dots'; | |
| for (let i = 0; i < 3; i++) { | |
| const dot = document.createElement('span'); | |
| dot.className = 'thinking-dot'; | |
| dotsWrap.appendChild(dot); | |
| } | |
| thinkingEl.appendChild(thinkingLabel); | |
| thinkingEl.appendChild(dotsWrap); | |
| thinkingEl.style.display = role === 'assistant' ? 'flex' : 'none'; | |
| // Main response text | |
| const text = document.createElement('p'); | |
| text.className = 'msg-text'; | |
| text.textContent = content; | |
| // Reasoning dropdown (hidden until thinking is done) | |
| const reasoningToggle = document.createElement('button'); | |
| reasoningToggle.className = 'reasoning-toggle'; | |
| reasoningToggle.innerHTML = 'βΆ Reasoning'; | |
| reasoningToggle.style.display = 'none'; | |
| const reasoningContent = document.createElement('div'); | |
| reasoningContent.className = 'reasoning-content'; | |
| reasoningContent.style.display = 'none'; | |
| reasoningToggle.addEventListener('click', () => { | |
| const isHidden = reasoningContent.style.display === 'none'; | |
| reasoningContent.style.display = isHidden ? 'block' : 'none'; | |
| reasoningToggle.innerHTML = isHidden ? 'βΌ Reasoning' : 'βΆ Reasoning'; | |
| }); | |
| contentWrapper.appendChild(thinkingEl); | |
| contentWrapper.appendChild(text); | |
| contentWrapper.appendChild(reasoningToggle); | |
| contentWrapper.appendChild(reasoningContent); | |
| div.appendChild(label); | |
| div.appendChild(contentWrapper); | |
| chatContainer.appendChild(div); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| return { text, thinkingEl, reasoningToggle, reasoningContent }; | |
| } | |
| // βββ PARSE RESPONSE βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Qwen3 thinking models output: <think>...reasoning...</think> actual response | |
| // We parse this in real-time as tokens stream in. | |
| function parseResponse(fullText, elements) { | |
| const { reasoningContent, reasoningToggle, thinkingEl } = elements || {}; | |
| const THINK_OPEN = '<think>'; | |
| const THINK_CLOSE = '</think>'; | |
| const startIdx = fullText.indexOf(THINK_OPEN); | |
| // No <think> tag found at all β plain response | |
| if (startIdx === -1) { | |
| if (thinkingEl) thinkingEl.style.display = 'none'; | |
| return fullText.trim(); | |
| } | |
| const endIdx = fullText.indexOf(THINK_CLOSE); | |
| if (endIdx === -1) { | |
| // Still streaming inside <think>...</think> β show animation, populate reasoning | |
| if (thinkingEl) thinkingEl.style.display = 'flex'; | |
| const reasoning = fullText.substring(startIdx + THINK_OPEN.length); | |
| if (reasoningContent) reasoningContent.textContent = reasoning; | |
| // Don't show toggle yet β we're still thinking | |
| if (reasoningToggle) reasoningToggle.style.display = 'none'; | |
| return ''; // No visible response yet | |
| } | |
| // </think> found β thinking is complete, extract both parts | |
| if (thinkingEl) thinkingEl.style.display = 'none'; | |
| const reasoning = fullText.substring(startIdx + THINK_OPEN.length, endIdx).trim(); | |
| const response = fullText.substring(endIdx + THINK_CLOSE.length).trim(); | |
| if (reasoningContent) reasoningContent.textContent = reasoning; | |
| if (reasoningToggle && reasoning.length > 0) { | |
| reasoningToggle.style.display = 'inline-flex'; | |
| } | |
| return response; | |
| } | |
| // βββ SEND βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function sendMessage() { | |
| if (!isLoaded || !engine) return; | |
| const userText = inputEl.value.trim(); | |
| if (!userText) return; | |
| inputEl.value = ''; | |
| inputEl.style.height = 'auto'; | |
| sendBtn.disabled = true; | |
| inputEl.disabled = true; | |
| addMessage('user', userText); | |
| chatHistory.push({ role: 'user', content: userText }); | |
| const aiElements = addMessage('assistant', ''); | |
| const { text, thinkingEl, reasoningToggle, reasoningContent } = aiElements; | |
| try { | |
| const stream = await engine.chat.completions.create({ | |
| messages: chatHistory, | |
| stream: true, | |
| temperature: 0.7, | |
| max_tokens: 1024, | |
| }); | |
| let fullResponse = ''; | |
| for await (const chunk of stream) { | |
| const delta = chunk.choices[0]?.delta?.content || ''; | |
| fullResponse += delta; | |
| const displayText = parseResponse(fullResponse, { | |
| reasoningContent, | |
| reasoningToggle, | |
| thinkingEl | |
| }); | |
| text.textContent = displayText; | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| // Final parse pass | |
| const finalText = parseResponse(fullResponse, { | |
| reasoningContent, | |
| reasoningToggle, | |
| thinkingEl | |
| }); | |
| text.textContent = finalText; | |
| // Ensure thinking anim is hidden | |
| thinkingEl.style.display = 'none'; | |
| // Store only the actual response (no <think> tags) in history | |
| chatHistory.push({ role: 'assistant', content: finalText }); | |
| } catch(err) { | |
| thinkingEl.style.display = 'none'; | |
| text.textContent = `Error: ${err.message}`; | |
| } | |
| sendBtn.disabled = false; | |
| inputEl.disabled = false; | |
| inputEl.focus(); | |
| } | |
| // βββ EVENTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| loadBtn.addEventListener('click', loadModel); | |
| sendBtn.addEventListener('click', sendMessage); | |
| inputEl.addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| $('clear-btn').addEventListener('click', () => { | |
| chatHistory = [ | |
| { | |
| role: 'system', | |
| 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.' | |
| } | |
| ]; | |
| chatContainer.innerHTML = ''; | |
| addSystemMessage('Conversation cleared. Model still loaded.'); | |
| }); | |
| // βββ INIT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| window.addEventListener('load', () => { | |
| checkCache(); | |
| if (!navigator.gpu) { | |
| statusEl.textContent = 'β οΈ WebGPU required β use Chrome 113+ / Edge 113+'; | |
| loadBtn.disabled = true; | |
| loadBtn.title = 'WebGPU not available in this browser'; | |
| } | |
| }); | |
| </script> | |
| <style> | |
| @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'); | |
| :root { | |
| --bg: #080808; | |
| --surface: #0f0f0f; | |
| --surface2: #141414; | |
| --border: #1e1e1e; | |
| --border2: #2a2a2a; | |
| --accent: #c8ff00; | |
| --accent2: #00ffc8; | |
| --text: #ddd; | |
| --muted: #444; | |
| --ai-border: #2a3a0a; | |
| --ai-bg: #0d1205; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'DM Sans', sans-serif; | |
| font-weight: 300; | |
| height: 100dvh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| /* HEADER */ | |
| header { | |
| padding: 14px 20px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: var(--surface); | |
| flex-shrink: 0; | |
| } | |
| .logo { display: flex; align-items: baseline; gap: 10px; } | |
| .logo-text { | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: 26px; | |
| letter-spacing: 4px; | |
| color: var(--accent); | |
| line-height: 1; | |
| } | |
| .logo-badge { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 9px; | |
| color: #000; | |
| background: var(--accent); | |
| padding: 2px 6px; | |
| letter-spacing: 1px; | |
| } | |
| .header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; } | |
| #cache-indicator { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 10px; | |
| color: var(--muted); | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| transition: color 0.4s; | |
| } | |
| #cache-indicator.has-cache { color: var(--accent); } | |
| .dot { | |
| width: 5px; height: 5px; border-radius: 50%; | |
| background: var(--muted); display: inline-block; | |
| animation: blink 2s infinite; | |
| } | |
| .dot.cached { background: var(--accent); animation: none; } | |
| @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} } | |
| #storage-info { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 9px; | |
| color: #2a2a2a; | |
| } | |
| /* STATUS BAR */ | |
| .status-bar { | |
| padding: 7px 20px; | |
| background: var(--surface2); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| flex-shrink: 0; | |
| min-height: 40px; | |
| } | |
| #status { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 10px; | |
| color: var(--muted); | |
| flex: 1; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| #progress { display: none; align-items: center; gap: 10px; width: 260px; flex-shrink: 0; } | |
| .progress-track { | |
| flex: 1; height: 2px; | |
| background: var(--border2); | |
| border-radius: 1px; overflow: hidden; | |
| } | |
| #progress-bar { | |
| height: 100%; background: var(--accent); width: 0%; | |
| transition: width 0.4s ease; | |
| box-shadow: 0 0 6px var(--accent); | |
| } | |
| #progress-text { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 9px; | |
| color: var(--accent); | |
| width: 50px; text-align: right; | |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | |
| } | |
| .model-tag { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 9px; | |
| color: #2e2e2e; | |
| border: 1px solid #1a1a1a; | |
| padding: 3px 8px; | |
| flex-shrink: 0; | |
| } | |
| #load-btn { | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: 14px; | |
| letter-spacing: 2px; | |
| background: var(--accent); | |
| color: #000; | |
| border: none; | |
| padding: 7px 18px; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| flex-shrink: 0; | |
| } | |
| #load-btn:hover:not(:disabled) { | |
| background: var(--accent2); | |
| box-shadow: 0 0 18px rgba(200,255,0,0.25); | |
| } | |
| #load-btn:disabled { opacity: 0.3; cursor: not-allowed; } | |
| /* CHAT */ | |
| #chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 14px; | |
| position: relative; | |
| } | |
| #chat-container::-webkit-scrollbar { width: 3px; } | |
| #chat-container::-webkit-scrollbar-thumb { background: #1e1e1e; } | |
| /* WELCOME */ | |
| #welcome { | |
| position: absolute; | |
| inset: 0; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 24px; | |
| pointer-events: none; | |
| user-select: none; | |
| } | |
| .welcome-title { | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: clamp(48px, 8vw, 80px); | |
| letter-spacing: 8px; | |
| color: #141414; | |
| line-height: 1; | |
| text-align: center; | |
| } | |
| .welcome-features { | |
| display: flex; | |
| gap: 0; | |
| border: 1px solid #141414; | |
| } | |
| .wf { | |
| padding: 8px 16px; | |
| border-right: 1px solid #141414; | |
| text-align: center; | |
| } | |
| .wf:last-child { border-right: none; } | |
| .wf-icon { font-size: 16px; margin-bottom: 4px; opacity: 0.3; } | |
| .wf-text { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 9px; | |
| color: #1e1e1e; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| line-height: 1.6; | |
| } | |
| /* MESSAGES */ | |
| .msg { max-width: 680px; animation: fadeUp 0.2s ease; } | |
| @keyframes fadeUp { | |
| from { opacity: 0; transform: translateY(6px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .system-msg { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 10px; | |
| color: #2a2a2a; | |
| align-self: center; | |
| text-align: center; | |
| max-width: 100%; | |
| padding: 4px 0; | |
| } | |
| .user-msg { | |
| align-self: flex-end; | |
| background: #131313; | |
| border: 1px solid var(--border2); | |
| border-radius: 1px 1px 0 1px; | |
| padding: 12px 16px; | |
| } | |
| .assistant-msg { | |
| align-self: flex-start; | |
| background: var(--ai-bg); | |
| border: 1px solid var(--ai-border); | |
| border-left: 2px solid var(--accent); | |
| border-radius: 1px 1px 1px 0; | |
| padding: 12px 16px; | |
| } | |
| .msg-label { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 8px; | |
| letter-spacing: 2px; | |
| color: var(--muted); | |
| display: block; | |
| margin-bottom: 6px; | |
| } | |
| .assistant-msg .msg-label { color: var(--accent); opacity: 0.5; } | |
| .msg p { | |
| font-size: 13.5px; | |
| line-height: 1.75; | |
| font-weight: 300; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| } | |
| .msg-content-wrapper { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| /* ββ THINKING ANIMATION ββ */ | |
| .thinking-indicator { | |
| display: none; /* shown via JS when inside <think> */ | |
| align-items: center; | |
| gap: 8px; | |
| padding: 2px 0 4px; | |
| } | |
| .thinking-label { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 9px; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| opacity: 0.6; | |
| } | |
| .thinking-dots { display: flex; gap: 4px; align-items: center; } | |
| .thinking-dot { | |
| width: 5px; | |
| height: 5px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| animation: thinkBounce 1.3s ease-in-out infinite both; | |
| } | |
| .thinking-dot:nth-child(1) { animation-delay: 0s; } | |
| .thinking-dot:nth-child(2) { animation-delay: 0.18s; } | |
| .thinking-dot:nth-child(3) { animation-delay: 0.36s; } | |
| @keyframes thinkBounce { | |
| 0%, 80%, 100% { transform: scale(0.55); opacity: 0.25; } | |
| 40% { transform: scale(1); opacity: 1; } | |
| } | |
| /* ββ REASONING DROPDOWN ββ */ | |
| .reasoning-toggle { | |
| display: none; /* shown via JS after </think> */ | |
| align-items: center; | |
| gap: 5px; | |
| background: transparent; | |
| border: 1px solid var(--border2); | |
| color: var(--muted); | |
| font-family: 'DM Mono', monospace; | |
| font-size: 9px; | |
| padding: 4px 10px; | |
| cursor: pointer; | |
| border-radius: 2px; | |
| align-self: flex-start; | |
| transition: all 0.2s; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .reasoning-toggle:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| .reasoning-content { | |
| margin-top: 2px; | |
| padding: 10px 12px; | |
| background: rgba(0,0,0,0.3); | |
| border: 1px solid var(--border2); | |
| border-left: 2px solid var(--accent2); | |
| border-radius: 2px; | |
| font-size: 11.5px; | |
| line-height: 1.6; | |
| color: #666; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| font-family: 'DM Mono', monospace; | |
| } | |
| /* INPUT */ | |
| .input-area { | |
| border-top: 1px solid var(--border); | |
| padding: 14px 20px; | |
| background: var(--surface); | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-end; | |
| flex-shrink: 0; | |
| } | |
| #user-input { | |
| flex: 1; | |
| background: var(--surface2); | |
| border: 1px solid var(--border2); | |
| color: var(--text); | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 13.5px; | |
| font-weight: 300; | |
| padding: 11px 14px; | |
| resize: none; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| min-height: 42px; | |
| max-height: 120px; | |
| border-radius: 0; | |
| line-height: 1.5; | |
| } | |
| #user-input:focus { border-color: var(--accent); } | |
| #user-input:disabled { opacity: 0.25; } | |
| #user-input::placeholder { color: #2a2a2a; } | |
| #send-btn { | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: 14px; | |
| letter-spacing: 2px; | |
| background: transparent; | |
| color: var(--accent); | |
| border: 1px solid var(--accent); | |
| padding: 9px 18px; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| height: 42px; | |
| flex-shrink: 0; | |
| } | |
| #send-btn:hover:not(:disabled) { | |
| background: var(--accent); | |
| color: #000; | |
| box-shadow: 0 0 14px rgba(200,255,0,0.15); | |
| } | |
| #send-btn:disabled { opacity: 0.15; cursor: not-allowed; } | |
| #clear-btn { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 9px; | |
| letter-spacing: 1px; | |
| background: transparent; | |
| color: var(--muted); | |
| border: 1px solid var(--border2); | |
| padding: 9px 12px; | |
| cursor: pointer; | |
| height: 42px; | |
| transition: all 0.15s; | |
| flex-shrink: 0; | |
| text-transform: uppercase; | |
| } | |
| #clear-btn:hover { border-color: #444; color: #888; } | |
| /* SPECS BAR */ | |
| .specs { | |
| display: flex; | |
| border-top: 1px solid var(--border); | |
| flex-shrink: 0; | |
| overflow-x: auto; | |
| } | |
| .spec-item { | |
| flex: 1; | |
| padding: 7px 14px; | |
| border-right: 1px solid var(--border); | |
| min-width: 90px; | |
| } | |
| .spec-item:last-child { border-right: none; } | |
| .spec-label { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 8px; | |
| color: #222; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| margin-bottom: 2px; | |
| } | |
| .spec-val { | |
| font-family: 'DM Mono', monospace; | |
| font-size: 10px; | |
| color: #333; | |
| } | |
| .spec-val.green { color: #5a8a00; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo"> | |
| <span class="logo-text">LOCAL MIND</span> | |
| <span class="logo-badge">OFFLINE</span> | |
| </div> | |
| <div class="header-right"> | |
| <div id="cache-indicator"><span class="dot"></span> Checking cache...</div> | |
| <div id="storage-info"></div> | |
| </div> | |
| </header> | |
| <div class="status-bar"> | |
| <span id="status">Ready β click Load Model to initialize</span> | |
| <div id="progress"> | |
| <div class="progress-track"><div id="progress-bar"></div></div> | |
| <span id="progress-text"></span> | |
| </div> | |
| <div class="model-tag">Qwen3-0.6B Β· q4f16_1 Β· MLC</div> | |
| <button id="load-btn">Load Model</button> | |
| </div> | |
| <div id="chat-container"> | |
| <div id="welcome"> | |
| <div class="welcome-title">YOUR BRAIN<br>YOUR DEVICE</div> | |
| <div class="welcome-features"> | |
| <div class="wf"> | |
| <div class="wf-icon">β‘</div> | |
| <div class="wf-text">WebGPU<br>Accelerated</div> | |
| </div> | |
| <div class="wf"> | |
| <div class="wf-icon">πΎ</div> | |
| <div class="wf-text">One-Time<br>Download</div> | |
| </div> | |
| <div class="wf"> | |
| <div class="wf-icon">π</div> | |
| <div class="wf-text">Zero Data<br>Leaves Browser</div> | |
| </div> | |
| <div class="wf"> | |
| <div class="wf-icon">βΎοΈ</div> | |
| <div class="wf-text">Cached<br>Forever</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="input-area"> | |
| <textarea | |
| id="user-input" | |
| placeholder="Load model first..." | |
| disabled | |
| rows="1" | |
| oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,120)+'px'" | |
| ></textarea> | |
| <button id="clear-btn">Clear</button> | |
| <button id="send-btn" disabled>Send</button> | |
| </div> | |
| <div class="specs"> | |
| <div class="spec-item"> | |
| <div class="spec-label">Inference</div> | |
| <div class="spec-val green">WebGPU</div> | |
| </div> | |
| <div class="spec-item"> | |
| <div class="spec-label">Cache</div> | |
| <div class="spec-val green">Browser Cache API</div> | |
| </div> | |
| <div class="spec-item"> | |
| <div class="spec-label">Privacy</div> | |
| <div class="spec-val green">100% Local</div> | |
| </div> | |
| <div class="spec-item"> | |
| <div class="spec-label">Download</div> | |
| <div class="spec-val">One-Time ~600MB</div> | |
| </div> | |
| <div class="spec-item"> | |
| <div class="spec-label">Runtime</div> | |
| <div class="spec-val">MLC / WebLLM</div> | |
| </div> | |
| <div class="spec-item"> | |
| <div class="spec-label">Build</div> | |
| <div class="spec-val">Zero β Open in Browser</div> | |
| </div> | |
| </div> | |
| </body> | |
| </html> |