Spaces:
Running
Running
Download index.html from andreolf/in-browser-chat: direct link, hf CLI and curl.
- Browser
- Download file 5.49 kB
-
https://huggingface.co/spaces/andreolf/in-browser-chat/resolve/main/index.html
- Command line
-
hf download hf://spaces/andreolf/in-browser-chat/index.html
-
curl -L -o index.html https://huggingface.co/spaces/andreolf/in-browser-chat/resolve/main/index.html
5.49 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>In-Browser Chat — SmolLM2-360M</title> | |
| <style> | |
| :root { --bg:#0f1020; --card:#1a1b34; --ink:#eef; --muted:#9aa; --accent:#8b7bff; | |
| --user:#2b2d55; --bot:#20223f; } | |
| * { box-sizing:border-box; } | |
| html,body { margin:0; height:100%; } | |
| body { font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:var(--bg); | |
| color:var(--ink); display:flex; flex-direction:column; height:100vh; } | |
| header { padding:14px 18px; border-bottom:1px solid #262744; } | |
| header h1 { margin:0; font-size:17px; } | |
| header p { margin:4px 0 0; font-size:12px; color:var(--muted); } | |
| header a { color:var(--accent); text-decoration:none; } | |
| #status { padding:10px 18px; font-size:13px; color:var(--muted); background:var(--card); } | |
| #bar { height:4px; background:#262744; border-radius:2px; overflow:hidden; margin-top:8px; display:none; } | |
| #bar > div { height:100%; width:0%; background:linear-gradient(90deg,#8b7bff,#5b8bff); transition:width .2s; } | |
| #chat { flex:1; overflow-y:auto; padding:18px; display:flex; flex-direction:column; gap:12px; } | |
| .msg { max-width:80%; padding:10px 14px; border-radius:14px; font-size:14px; line-height:1.5; white-space:pre-wrap; } | |
| .user { align-self:flex-end; background:var(--user); border-bottom-right-radius:4px; } | |
| .bot { align-self:flex-start; background:var(--bot); border-bottom-left-radius:4px; } | |
| footer { display:flex; gap:8px; padding:14px 18px; border-top:1px solid #262744; } | |
| #input { flex:1; background:var(--card); color:var(--ink); border:1px solid #333; border-radius:10px; | |
| padding:10px 12px; font-size:14px; resize:none; } | |
| button { background:var(--accent); color:#fff; border:0; border-radius:10px; padding:0 18px; | |
| font-size:14px; font-weight:600; cursor:pointer; } | |
| button:disabled { opacity:.5; cursor:not-allowed; } | |
| .hint { font-size:11px; color:var(--muted); } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>🤖 In-Browser Chat — SmolLM2-360M</h1> | |
| <p>Runs <b>100% in your browser</b> via | |
| <a href="https://github.com/huggingface/transformers.js" target="_blank">transformers.js</a>. | |
| No server, no API calls — the model | |
| (<a href="https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct" target="_blank">SmolLM2-360M-Instruct</a>) | |
| downloads once and generates locally. Uses WebGPU when available, WASM otherwise.</p> | |
| </header> | |
| <div id="status">Click send to load the model (~300 MB, cached after first load). | |
| <div id="bar"><div></div></div> | |
| </div> | |
| <div id="chat"></div> | |
| <footer> | |
| <textarea id="input" rows="1" placeholder="Say hello…"></textarea> | |
| <button id="send">Send</button> | |
| </footer> | |
| <script type="module"> | |
| import { pipeline, TextStreamer } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3"; | |
| const chat = document.getElementById("chat"); | |
| const input = document.getElementById("input"); | |
| const send = document.getElementById("send"); | |
| const status = document.getElementById("status"); | |
| const bar = document.getElementById("bar"); | |
| const barFill = bar.firstElementChild; | |
| let generator = null; | |
| let loading = false; | |
| const messages = [{ role: "system", content: "You are a friendly, concise assistant." }]; | |
| function addMsg(role, text) { | |
| const d = document.createElement("div"); | |
| d.className = "msg " + (role === "user" ? "user" : "bot"); | |
| d.textContent = text; | |
| chat.appendChild(d); | |
| chat.scrollTop = chat.scrollHeight; | |
| return d; | |
| } | |
| async function ensureModel() { | |
| if (generator) return generator; | |
| loading = true; | |
| send.disabled = true; | |
| bar.style.display = "block"; | |
| const hasGPU = "gpu" in navigator; | |
| status.firstChild.textContent = | |
| `Loading SmolLM2-360M on ${hasGPU ? "WebGPU" : "WASM"}… `; | |
| generator = await pipeline("text-generation", "HuggingFaceTB/SmolLM2-360M-Instruct", { | |
| dtype: hasGPU ? "q4f16" : "q4", | |
| device: hasGPU ? "webgpu" : "wasm", | |
| progress_callback: (p) => { | |
| if (p.status === "progress" && p.total) { | |
| const pct = Math.round((p.loaded / p.total) * 100); | |
| barFill.style.width = pct + "%"; | |
| status.firstChild.textContent = `Downloading ${p.file?.split("/").pop() || ""} — ${pct}% `; | |
| } | |
| }, | |
| }); | |
| bar.style.display = "none"; | |
| status.firstChild.textContent = "Model ready — chatting locally in your browser. "; | |
| loading = false; | |
| send.disabled = false; | |
| return generator; | |
| } | |
| async function run() { | |
| const text = input.value.trim(); | |
| if (!text || loading) return; | |
| input.value = ""; | |
| addMsg("user", text); | |
| messages.push({ role: "user", content: text }); | |
| const gen = await ensureModel(); | |
| const botEl = addMsg("bot", ""); | |
| send.disabled = true; | |
| let acc = ""; | |
| const streamer = new TextStreamer(gen.tokenizer, { | |
| skip_prompt: true, | |
| skip_special_tokens: true, | |
| callback_function: (t) => { acc += t; botEl.textContent = acc; chat.scrollTop = chat.scrollHeight; }, | |
| }); | |
| const out = await gen(messages, { max_new_tokens: 256, do_sample: true, temperature: 0.7, top_p: 0.9, streamer }); | |
| const full = out[0].generated_text.at(-1).content; | |
| botEl.textContent = full; | |
| messages.push({ role: "assistant", content: full }); | |
| send.disabled = false; | |
| input.focus(); | |
| } | |
| send.addEventListener("click", run); | |
| input.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); run(); } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |