Spaces:
Running
Running
Download index.html from QuantumHawk42/gemma-3-1b-WebGPU: direct link, hf CLI and curl.
- Browser
- Download file 17.4 kB
-
https://huggingface.co/spaces/QuantumHawk42/gemma-3-1b-WebGPU/resolve/main/index.html
- Command line
-
hf download hf://spaces/QuantumHawk42/gemma-3-1b-WebGPU/index.html
-
curl -L -o index.html https://huggingface.co/spaces/QuantumHawk42/gemma-3-1b-WebGPU/resolve/main/index.html
17.4 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Gemma 3 1B Chat</title> | |
| <style> | |
| :root { | |
| color-scheme: light; | |
| --bg: #f6f3ee; | |
| --panel: #ffffff; | |
| --line: #e7dfd4; | |
| --text: #1f1a16; | |
| --muted: #6d6256; | |
| --accent: #b35a2b; | |
| --accent-soft: #f7eee8; | |
| --user: #f3ede5; | |
| --assistant: #fcfaf7; | |
| --error: #a53f2b; | |
| --shadow: 0 16px 48px rgba(36, 24, 15, 0.08); | |
| --radius: 22px; | |
| --radius-sm: 16px; | |
| --radius-pill: 999px; | |
| --sans: "Avenir Next", "Segoe UI", sans-serif; | |
| --mono: "SFMono-Regular", Consolas, monospace; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: var(--sans); | |
| background: linear-gradient(180deg, #faf7f2 0%, var(--bg) 100%); | |
| color: var(--text); | |
| display: grid; | |
| place-items: center; | |
| padding: 20px; | |
| } | |
| .app { | |
| width: min(760px, 100%); | |
| height: min(840px, calc(100vh - 40px)); | |
| background: var(--panel); | |
| border: 1px solid var(--line); | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow); | |
| display: grid; | |
| grid-template-rows: auto 1fr auto; | |
| overflow: hidden; | |
| } | |
| .header { | |
| padding: 18px 20px; | |
| border-bottom: 1px solid var(--line); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 16px; | |
| } | |
| .title { | |
| margin: 0; | |
| font-size: 1rem; | |
| font-weight: 700; | |
| } | |
| .subtitle { | |
| margin: 0; | |
| color: var(--muted); | |
| font-size: 0.92rem; | |
| text-align: right; | |
| } | |
| .spinner { | |
| width: 18px; | |
| height: 18px; | |
| border: 2px solid rgba(179, 90, 43, 0.2); | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| flex: 0 0 auto; | |
| } | |
| .spinner.hidden { | |
| display: none; | |
| } | |
| .chat { | |
| padding: 18px; | |
| overflow: auto; | |
| background: #fcfbf8; | |
| display: grid; | |
| align-content: start; | |
| gap: 12px; | |
| position: relative; | |
| } | |
| .message { | |
| max-width: 85%; | |
| padding: 14px 16px; | |
| border-radius: 18px; | |
| line-height: 1.55; | |
| white-space: pre-wrap; | |
| border: 1px solid var(--line); | |
| } | |
| .message.user { | |
| margin-left: auto; | |
| background: var(--user); | |
| } | |
| .message.assistant { | |
| background: var(--assistant); | |
| } | |
| .message.placeholder { | |
| max-width: 100%; | |
| color: var(--muted); | |
| background: #fffdfb; | |
| text-align: left; | |
| padding: 18px; | |
| } | |
| .message.placeholder strong { | |
| display: block; | |
| margin-bottom: 10px; | |
| color: var(--text); | |
| } | |
| .examples { | |
| display: grid; | |
| gap: 10px; | |
| } | |
| .example { | |
| width: 100%; | |
| text-align: left; | |
| min-width: 0; | |
| padding: 12px 14px; | |
| border-radius: 14px; | |
| border: 1px solid var(--line); | |
| background: #faf6f1; | |
| color: var(--text); | |
| font-weight: 500; | |
| } | |
| .example:hover:not(:disabled) { | |
| background: var(--accent-soft); | |
| } | |
| .center-state { | |
| position: absolute; | |
| inset: 0; | |
| display: grid; | |
| place-items: center; | |
| padding: 24px; | |
| text-align: center; | |
| color: var(--muted); | |
| } | |
| .center-card { | |
| display: grid; | |
| justify-items: center; | |
| gap: 14px; | |
| } | |
| .center-card .spinner { | |
| width: 28px; | |
| height: 28px; | |
| border-width: 3px; | |
| } | |
| .composer { | |
| padding: 16px; | |
| border-top: 1px solid var(--line); | |
| background: var(--panel); | |
| display: grid; | |
| gap: 12px; | |
| } | |
| .toolbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| } | |
| .metrics { | |
| display: flex; | |
| justify-content: flex-end; | |
| color: var(--muted); | |
| font-size: 0.82rem; | |
| font-family: var(--mono); | |
| } | |
| .reset { | |
| border: 1px solid var(--line); | |
| background: #fffdfb; | |
| color: var(--muted); | |
| padding: 10px 14px; | |
| min-width: 0; | |
| } | |
| .input-row { | |
| position: relative; | |
| } | |
| textarea { | |
| width: 100%; | |
| min-height: 64px; | |
| max-height: 180px; | |
| resize: vertical; | |
| border: 1px solid var(--line); | |
| border-radius: var(--radius-sm); | |
| padding: 16px 110px 16px 16px; | |
| font: inherit; | |
| color: var(--text); | |
| background: #fffdfb; | |
| outline: none; | |
| } | |
| textarea:focus { | |
| border-color: #d8b49f; | |
| box-shadow: 0 0 0 4px rgba(179, 90, 43, 0.08); | |
| } | |
| button { | |
| appearance: none; | |
| border: 0; | |
| border-radius: var(--radius-pill); | |
| padding: 12px 16px; | |
| background: var(--accent); | |
| color: white; | |
| font: inherit; | |
| font-weight: 400; | |
| cursor: pointer; | |
| min-width: 84px; | |
| } | |
| .send { | |
| position: absolute; | |
| right: 10px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| z-index: 1; | |
| width: 44px; | |
| height: 44px; | |
| min-width: 44px; | |
| padding: 0; | |
| display: grid; | |
| place-items: center; | |
| border-radius: 14px; | |
| } | |
| .send svg { | |
| width: 18px; | |
| height: 18px; | |
| fill: currentColor; | |
| } | |
| .send.stop { | |
| background: #8b4a28; | |
| } | |
| button:disabled { | |
| cursor: not-allowed; | |
| opacity: 0.6; | |
| } | |
| .error { | |
| color: var(--error); | |
| } | |
| .cursor { | |
| display: inline-block; | |
| width: 0.75ch; | |
| color: var(--accent); | |
| animation: blink 1s step-end infinite; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| @keyframes blink { | |
| 50% { | |
| opacity: 0; | |
| } | |
| } | |
| @media (max-width: 720px) { | |
| .app { | |
| height: calc(100vh - 20px); | |
| } | |
| .header, | |
| .toolbar { | |
| align-items: start; | |
| flex-direction: column; | |
| } | |
| .subtitle { | |
| text-align: left; | |
| } | |
| .message { | |
| max-width: 92%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main class="app"> | |
| <header class="header"> | |
| <h1 class="title">Gemma 3 1B Chat</h1> | |
| <p class="subtitle">Powered by Transformers.js v4 (preview)</p> | |
| </header> | |
| <section id="chat" class="chat"> | |
| <div id="loadingState" class="center-state"> | |
| <div class="center-card"> | |
| <span class="spinner"></span> | |
| <div id="statusText">Loading model...</div> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="composer"> | |
| <div class="toolbar"> | |
| <button id="resetButton" class="reset" type="button" disabled> | |
| Reset | |
| </button> | |
| <div class="metrics" id="metrics">tokens/sec: -</div> | |
| </div> | |
| <div class="input-row"> | |
| <textarea | |
| id="prompt" | |
| placeholder="Enter text here" | |
| disabled | |
| ></textarea> | |
| <button | |
| id="sendButton" | |
| class="send" | |
| type="button" | |
| disabled | |
| aria-label="Send message" | |
| title="Send message" | |
| ></button> | |
| </div> | |
| </section> | |
| </main> | |
| <script type="module"> | |
| import { | |
| pipeline, | |
| TextStreamer, | |
| InterruptableStoppingCriteria, | |
| } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.0.0-next.7"; | |
| const MODEL_ID = "onnx-community/gemma-3-1b-it-ONNX"; | |
| const DTYPE = "q4"; | |
| const REVISION = "main"; | |
| const MAX_NEW_TOKENS = 256; | |
| const SYSTEM_PROMPT = | |
| "You are a helpful assistant who responds clearly and concisely."; | |
| const chatEl = document.getElementById("chat"); | |
| const promptEl = document.getElementById("prompt"); | |
| const sendButton = document.getElementById("sendButton"); | |
| const resetButton = document.getElementById("resetButton"); | |
| const statusTextEl = document.getElementById("statusText"); | |
| const metricsEl = document.getElementById("metrics"); | |
| const loadingStateEl = document.getElementById("loadingState"); | |
| const SEND_ICON = ` | |
| <svg viewBox="0 0 24 24" aria-hidden="true"> | |
| <path d="M3.4 20.6 21 13 3.4 5.4l.1 5.8 11 1.8-11 1.8z"></path> | |
| </svg> | |
| `; | |
| const STOP_ICON = ` | |
| <svg viewBox="0 0 24 24" aria-hidden="true"> | |
| <path d="M7 7h10v10H7z"></path> | |
| </svg> | |
| `; | |
| let generator = null; | |
| let isGenerating = false; | |
| let loadFailed = false; | |
| const stoppingCriteria = new InterruptableStoppingCriteria(); | |
| const conversation = [{ role: "system", content: SYSTEM_PROMPT }]; | |
| const examplePrompts = [ | |
| "What is the capital of France?", | |
| "Explain photosynthesis in one sentence.", | |
| "Write a poem about the ocean.", | |
| ]; | |
| function setStatus(message, { error = false } = {}) { | |
| statusTextEl.textContent = message; | |
| statusTextEl.classList.toggle("error", error); | |
| } | |
| function setMetrics(text) { | |
| metricsEl.textContent = text; | |
| } | |
| function scrollChatToBottom() { | |
| chatEl.scrollTop = chatEl.scrollHeight; | |
| } | |
| function removePlaceholder() { | |
| const placeholder = chatEl.querySelector(".placeholder"); | |
| if (placeholder) { | |
| placeholder.remove(); | |
| } | |
| } | |
| function clearCenterState() { | |
| if (loadingStateEl) { | |
| loadingStateEl.remove(); | |
| } | |
| } | |
| function showExamples() { | |
| clearCenterState(); | |
| removePlaceholder(); | |
| if (chatEl.querySelector(".examples")) { | |
| return; | |
| } | |
| const card = document.createElement("div"); | |
| card.className = "message assistant placeholder"; | |
| card.innerHTML = "<strong>Try one of these</strong>"; | |
| const list = document.createElement("div"); | |
| list.className = "examples"; | |
| for (const prompt of examplePrompts) { | |
| const button = document.createElement("button"); | |
| button.type = "button"; | |
| button.className = "example"; | |
| button.textContent = prompt; | |
| button.addEventListener("click", () => { | |
| promptEl.value = prompt; | |
| sendMessage(prompt); | |
| }); | |
| list.appendChild(button); | |
| } | |
| card.appendChild(list); | |
| chatEl.appendChild(card); | |
| } | |
| function addMessage(role, text = "") { | |
| removePlaceholder(); | |
| const node = document.createElement("div"); | |
| node.className = `message ${role}`; | |
| node.textContent = text; | |
| chatEl.appendChild(node); | |
| scrollChatToBottom(); | |
| return node; | |
| } | |
| function setStreamingMessage(node, text) { | |
| node.textContent = text; | |
| const cursor = document.createElement("span"); | |
| cursor.className = "cursor"; | |
| cursor.textContent = "▋"; | |
| node.appendChild(cursor); | |
| scrollChatToBottom(); | |
| } | |
| function updateComposer() { | |
| const ready = Boolean(generator) && !loadFailed; | |
| promptEl.disabled = !ready || isGenerating; | |
| sendButton.disabled = !ready; | |
| resetButton.disabled = !ready || isGenerating; | |
| sendButton.classList.toggle("stop", isGenerating); | |
| sendButton.innerHTML = isGenerating ? STOP_ICON : SEND_ICON; | |
| sendButton.setAttribute( | |
| "aria-label", | |
| isGenerating ? "Stop generation" : "Send message", | |
| ); | |
| sendButton.setAttribute( | |
| "title", | |
| isGenerating ? "Stop generation" : "Send message", | |
| ); | |
| } | |
| function resetChat() { | |
| if (isGenerating) { | |
| return; | |
| } | |
| conversation.length = 1; | |
| chatEl.innerHTML = ""; | |
| showExamples(); | |
| setStatus("Ready"); | |
| setMetrics("tokens/sec: -"); | |
| promptEl.value = ""; | |
| promptEl.focus(); | |
| } | |
| async function loadModel() { | |
| if (!window.isSecureContext) { | |
| throw new Error( | |
| "WebGPU requires a secure context like https:// or localhost.", | |
| ); | |
| } | |
| if (!("gpu" in navigator)) { | |
| throw new Error("This browser does not support WebGPU."); | |
| } | |
| setStatus("Loading model..."); | |
| setMetrics("tokens/sec: -"); | |
| const startedAt = performance.now(); | |
| generator = await pipeline("text-generation", MODEL_ID, { | |
| dtype: DTYPE, | |
| revision: REVISION, | |
| device: "webgpu", | |
| progress_callback: (p) => { | |
| if (p.status !== "progress_total") return; | |
| const percent = Math.round(p.progress); | |
| setStatus(`Downloading model... ${percent}%`); | |
| }, | |
| }); | |
| const seconds = ((performance.now() - startedAt) / 1000).toFixed(1); | |
| setStatus(`Ready in ${seconds}s`); | |
| setMetrics("tokens/sec: -"); | |
| showExamples(); | |
| updateComposer(); | |
| } | |
| async function sendMessage(overrideText) { | |
| if (isGenerating) { | |
| stoppingCriteria.interrupt(); | |
| setStatus("Stopping..."); | |
| return; | |
| } | |
| const text = (overrideText ?? promptEl.value).trim(); | |
| if (!text || !generator) { | |
| return; | |
| } | |
| isGenerating = true; | |
| updateComposer(); | |
| stoppingCriteria.reset(); | |
| addMessage("user", text); | |
| const assistantNode = addMessage("assistant", ""); | |
| promptEl.value = ""; | |
| conversation.push({ role: "user", content: text }); | |
| let streamedText = ""; | |
| let tokenCount = 0; | |
| let decodeStartedAt = null; | |
| clearCenterState(); | |
| setStatus("Generating..."); | |
| setMetrics("tokens/sec: -"); | |
| try { | |
| const output = await generator(conversation, { | |
| max_new_tokens: MAX_NEW_TOKENS, | |
| do_sample: false, | |
| stopping_criteria: stoppingCriteria, | |
| streamer: new TextStreamer(generator.tokenizer, { | |
| skip_prompt: true, | |
| skip_special_tokens: true, | |
| callback_function: (chunk) => { | |
| streamedText += chunk; | |
| setStreamingMessage(assistantNode, streamedText); | |
| }, | |
| token_callback_function: () => { | |
| const now = performance.now(); | |
| if (decodeStartedAt === null) { | |
| decodeStartedAt = now; | |
| } | |
| tokenCount += 1; | |
| if (tokenCount < 2) { | |
| setMetrics("tokens/sec: -"); | |
| return; | |
| } | |
| const elapsedSeconds = Math.max( | |
| (now - decodeStartedAt) / 1000, | |
| 0.001, | |
| ); | |
| const tokensPerSecond = ( | |
| (tokenCount - 1) / | |
| elapsedSeconds | |
| ).toFixed(1); | |
| setMetrics(`tokens/sec: ${tokensPerSecond}`); | |
| }, | |
| }), | |
| }); | |
| const assistantText = | |
| output?.[0]?.generated_text?.at(-1)?.content?.trim() || | |
| streamedText.trim() || | |
| "No response generated."; | |
| assistantNode.textContent = assistantText; | |
| conversation.push({ role: "assistant", content: assistantText }); | |
| if (tokenCount >= 2 && decodeStartedAt !== null) { | |
| const elapsedSeconds = Math.max( | |
| (performance.now() - decodeStartedAt) / 1000, | |
| 0.001, | |
| ); | |
| const tokensPerSecond = ((tokenCount - 1) / elapsedSeconds).toFixed( | |
| 1, | |
| ); | |
| setMetrics(`tokens/sec: ${tokensPerSecond}`); | |
| } else { | |
| setMetrics("tokens/sec: -"); | |
| } | |
| setStatus("Ready"); | |
| } catch (error) { | |
| console.error(error); | |
| assistantNode.textContent = | |
| "Something went wrong while generating a response."; | |
| assistantNode.classList.add("error"); | |
| setStatus(error.message, { error: true }); | |
| setMetrics("tokens/sec: -"); | |
| } finally { | |
| isGenerating = false; | |
| updateComposer(); | |
| if (generator && !loadFailed) { | |
| promptEl.focus(); | |
| } | |
| } | |
| } | |
| sendButton.addEventListener("click", sendMessage); | |
| resetButton.addEventListener("click", resetChat); | |
| promptEl.addEventListener("keydown", (event) => { | |
| if (event.key === "Enter" && !event.shiftKey) { | |
| event.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| updateComposer(); | |
| loadModel().catch((error) => { | |
| console.error(error); | |
| loadFailed = true; | |
| setStatus(error.message, { error: true }); | |
| setMetrics("tokens/sec: -"); | |
| updateComposer(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |