| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Gemma 3 270M WebGPU Demo</title> |
| <style> |
| :root { |
| --bg-color: #0d1117; |
| --chat-bg: #161b22; |
| --text-color: #c9d1d9; |
| --accent: #238636; |
| --user-msg: #1f6feb; |
| } |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; |
| background-color: var(--bg-color); |
| color: var(--text-color); |
| margin: 0; |
| display: flex; |
| flex-direction: column; |
| height: 100vh; |
| } |
| header { |
| padding: 1rem; |
| border-bottom: 1px solid #30363d; |
| text-align: center; |
| } |
| #chat-container { |
| flex: 1; |
| padding: 1rem; |
| overflow-y: auto; |
| display: flex; |
| flex-direction: column; |
| gap: 1rem; |
| max-width: 800px; |
| margin: 0 auto; |
| width: 100%; |
| } |
| .message { |
| padding: 0.8rem 1.2rem; |
| border-radius: 8px; |
| max-width: 80%; |
| line-height: 1.5; |
| } |
| .user { |
| align-self: flex-end; |
| background-color: var(--user-msg); |
| color: white; |
| } |
| .assistant { |
| align-self: flex-start; |
| background-color: var(--chat-bg); |
| border: 1px solid #30363d; |
| } |
| #input-area { |
| padding: 1rem; |
| background-color: var(--chat-bg); |
| border-top: 1px solid #30363d; |
| display: flex; |
| gap: 10px; |
| justify-content: center; |
| } |
| input[type="text"] { |
| padding: 10px; |
| border-radius: 6px; |
| border: 1px solid #30363d; |
| background-color: var(--bg-color); |
| color: white; |
| flex: 1; |
| max-width: 600px; |
| } |
| button { |
| padding: 10px 20px; |
| border-radius: 6px; |
| border: none; |
| background-color: var(--accent); |
| color: white; |
| font-weight: bold; |
| cursor: pointer; |
| } |
| button:disabled { |
| background-color: #30363d; |
| cursor: not-allowed; |
| } |
| #status { |
| font-size: 0.8rem; |
| color: #8b949e; |
| text-align: center; |
| margin-top: 5px; |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <header> |
| <h2>Gemma 3 270M-it (WebGPU)</h2> |
| <div id="status">Converting model to engine...</div> |
| </header> |
|
|
| <div id="chat-container"> |
| <div class="message assistant"> |
| Hello! I am Gemma 3 (270M), running entirely in your browser using WebGPU. How can I help you today? |
| </div> |
| </div> |
|
|
| <div id="input-area"> |
| <input type="text" id="user-input" placeholder="Type a message..." disabled> |
| <button id="send-btn" disabled>Send</button> |
| </div> |
|
|
| <script type="module"> |
| import { CreateMLCEngine } from "https://esm.run/@mlc-ai/web-llm"; |
| |
| let engine; |
| const chatContainer = document.getElementById('chat-container'); |
| const userInput = document.getElementById('user-input'); |
| const sendBtn = document.getElementById('send-btn'); |
| const statusLabel = document.getElementById('status'); |
| |
| |
| |
| |
| const appConfig = { |
| model_list: [ |
| { |
| "model": "http://localhost:8000/gemma-3-270m-it-mlc", |
| "model_id": "gemma-3-270m-it", |
| "model_lib": "http://localhost:8000/libs/gemma-3-270m-it-webgpu.wasm", |
| "overrides": { |
| "context_window_size": 2048 |
| } |
| } |
| ] |
| }; |
| |
| async function init() { |
| try { |
| statusLabel.textContent = "Loading WebGPU Engine (this may take a moment)..."; |
| |
| engine = await CreateMLCEngine("gemma-3-270m-it", { |
| appConfig, |
| initProgressCallback: (report) => { |
| statusLabel.textContent = report.text; |
| } |
| }); |
| |
| statusLabel.textContent = "Ready"; |
| userInput.disabled = false; |
| sendBtn.disabled = false; |
| userInput.focus(); |
| } catch (err) { |
| statusLabel.textContent = "Error: " + err.message; |
| console.error(err); |
| appendMessage("assistant", "Error loading model. Make sure you are serving the files at http://localhost:8000 and your browser supports WebGPU."); |
| } |
| } |
| |
| function appendMessage(role, text) { |
| const msgDiv = document.createElement('div'); |
| msgDiv.className = `message ${role}`; |
| msgDiv.textContent = text; |
| chatContainer.appendChild(msgDiv); |
| chatContainer.scrollTop = chatContainer.scrollHeight; |
| return msgDiv; |
| } |
| |
| async function handleSend() { |
| const text = userInput.value.trim(); |
| if (!text) return; |
| |
| appendMessage("user", text); |
| userInput.value = ""; |
| userInput.disabled = true; |
| sendBtn.disabled = true; |
| |
| const assistantMsgDiv = appendMessage("assistant", "Thinking..."); |
| |
| try { |
| const chunks = await engine.chat.completions.create({ |
| messages: [ |
| { role: "user", content: text } |
| ], |
| stream: true, |
| }); |
| |
| let fullResponse = ""; |
| for await (const chunk of chunks) { |
| const content = chunk.choices[0]?.delta?.content || ""; |
| fullResponse += content; |
| assistantMsgDiv.textContent = fullResponse; |
| chatContainer.scrollTop = chatContainer.scrollHeight; |
| } |
| |
| |
| const stats = await engine.runtimeStatsText(); |
| statusLabel.textContent = stats; |
| |
| } catch (err) { |
| assistantMsgDiv.textContent += "\n[Error generating response]"; |
| console.error(err); |
| } finally { |
| userInput.disabled = false; |
| sendBtn.disabled = false; |
| userInput.focus(); |
| } |
| } |
| |
| sendBtn.addEventListener('click', handleSend); |
| userInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') handleSend(); |
| }); |
| |
| init(); |
| </script> |
|
|
| </body> |
| </html> |
|
|