Spaces:
Running
Running
Download index.html from StrongTheDev/chat-with-rick: direct link, hf CLI and curl.
- Browser
- Download file 9.8 kB
-
https://huggingface.co/spaces/StrongTheDev/chat-with-rick/resolve/main/index.html
- Command line
-
hf download hf://spaces/StrongTheDev/chat-with-rick/index.html
-
curl -L -o index.html https://huggingface.co/spaces/StrongTheDev/chat-with-rick/resolve/main/index.html
9.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Chat with Rick</title> | |
| <style> | |
| :root{--bg:#0f1a14;--panel:#16251c;--me:#2b6cb0;--rick:#1f3a2b;--fg:#e6f4ea;--mut:#8fb09b;--acc:#97ce4c;--line:#2f5440} | |
| *{box-sizing:border-box} | |
| body{margin:0;height:100vh;display:flex;flex-direction:column;background:var(--bg);color:var(--fg);font:16px/1.4 system-ui,sans-serif} | |
| header{padding:14px 18px;background:var(--panel);border-bottom:1px solid #263f31} | |
| h1{margin:0;font-size:1.2rem;color:var(--acc)} | |
| header p{margin:2px 0 0;color:var(--mut);font-size:.85rem} | |
| .wrap{max-width:820px;width:100%;margin:0 auto} | |
| #log{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:10px} | |
| .m{padding:10px 14px;border-radius:14px;max-width:80%;white-space:pre-wrap;word-wrap:break-word} | |
| .me{align-self:flex-end;background:var(--me)} | |
| .rick{align-self:flex-start;background:var(--rick);border:1px solid var(--line)} | |
| .sys{align-self:center;color:var(--mut);font-size:.85rem;text-align:center} | |
| .chips{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;padding:0 12px 8px} | |
| .chips span{padding:5px 10px;border:1px solid var(--line);border-radius:999px;font-size:.82rem;color:var(--mut);cursor:pointer} | |
| .chips span:hover{color:var(--acc);border-color:var(--acc)} | |
| #bar{height:3px;background:#263f31;visibility:hidden}#fill{height:100%;width:0;background:var(--acc);transition:width .2s} | |
| #dl{color:var(--mut);font-size:.8rem;text-align:center;min-height:1.1em} | |
| form{display:flex;gap:8px;padding:8px 12px 14px;align-items:center} | |
| select,input[type=text]{padding:11px 10px;border-radius:10px;border:1px solid var(--line);background:var(--panel);color:var(--fg);font:inherit} | |
| select{max-width:210px;font-size:.88rem} | |
| input[type=text]{flex:1;min-width:0} | |
| button{padding:11px 18px;border:0;border-radius:10px;background:var(--acc);color:#10200f;font:inherit;font-weight:600;cursor:pointer} | |
| :disabled{opacity:.5;cursor:not-allowed} | |
| @media(max-width:560px){form{flex-wrap:wrap}select{max-width:none;flex:1}input[type=text]{flex-basis:100%;order:3}button{order:4}} | |
| </style> | |
| </head> | |
| <body> | |
| <header class="wrap"> | |
| <h1>🧪 Chat with Rick</h1> | |
| <p>DialoGPT-medium fine-tuned on Rick & Morty, running in your browser.</p> | |
| </header> | |
| <div id="log" class="wrap"><div class="m sys" id="status">Checking available weights…</div></div> | |
| <div class="chips wrap" id="chips"> | |
| <span>Hey Rick, what are we doing today?</span> | |
| <span>Tell me about the portal gun.</span> | |
| <span>Morty is worried about school.</span> | |
| </div> | |
| <div class="wrap"><div id="bar"><div id="fill"></div></div><div id="dl"></div></div> | |
| <form id="f" class="wrap"> | |
| <select id="dtype" title="Model weights" disabled></select> | |
| <select id="device" title="Run on" style="display:none"></select> | |
| <input type="text" id="in" placeholder="Say something to Rick…" autocomplete="off" disabled> | |
| <button id="go" disabled>Send</button> | |
| </form> | |
| <script type="module"> | |
| import { pipeline, TextStreamer, env } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3"; | |
| const MODEL = "onnx-community/DialoGPT-medium-Rick-from-RickAndMorty-ONNX"; | |
| const $ = id => document.getElementById(id); | |
| try { env.backends.onnx.wasm.proxy = true; } catch {} // run CPU inference in a worker | |
| const paint = () => new Promise(r => { requestAnimationFrame(() => setTimeout(r, 0)); setTimeout(r, 200); }); | |
| const DTYPES = { // dev: where the variant runs correctly (int8-style quant ops are broken on WebGPU) | |
| int8: {suffix:"_int8", label:"int8", dev:"cpu"}, | |
| uint8: {suffix:"_uint8", label:"uint8", dev:"cpu"}, | |
| q8: {suffix:"_quantized", label:"q8", dev:"cpu"}, | |
| bnb4: {suffix:"_bnb4", label:"bnb4", dev:"cpu"}, | |
| q4: {suffix:"_q4", label:"q4", dev:"both"}, | |
| fp32: {suffix:"", label:"fp32", dev:"both"}, | |
| q4f16: {suffix:"_q4f16", label:"q4f16", dev:"gpu"}, | |
| fp16: {suffix:"_fp16", label:"fp16", dev:"gpu"}, | |
| }; | |
| const PREF = {wasm:["int8","uint8","q8","q4","fp32"], webgpu:["q4f16","fp16","q4","fp32"]}; | |
| const okOn = (key, device) => { const d = DTYPES[key].dev; return d === "both" || (d === "gpu") === (device === "webgpu"); }; | |
| const DEFAULT_ORDER = PREF.wasm; | |
| const fmt = b => b == null ? "size unknown" : b >= 1e9 ? (b/1e9).toFixed(2)+" GB" : (b/1e6).toFixed(0)+" MB"; | |
| const hasGPU = !!navigator.gpu; | |
| let available = [], pipe = null, current = null, currentDevice = null, history = [], busy = false; | |
| const dtSel = $("dtype"), devSel = $("device"), input = $("in"), btn = $("go"); | |
| const add = (t, cls) => { const d = document.createElement("div"); d.className = "m " + cls; d.textContent = t; | |
| $("log").appendChild(d); $("log").scrollTop = $("log").scrollHeight; return d; }; | |
| const lock = on => { dtSel.disabled = devSel.disabled = input.disabled = btn.disabled = on; }; | |
| /* ---------- discover variants + sizes from the Hub API ---------- */ | |
| async function discover() { | |
| let files = null; | |
| try { | |
| const r = await fetch(`https://huggingface.co/api/models/${MODEL}/tree/main/onnx`); | |
| if (r.ok) files = Object.fromEntries((await r.json()).filter(f => f.type === "file").map(f => [f.path, f.lfs?.size ?? f.size])); | |
| } catch (e) { console.warn("File listing failed", e); } | |
| for (const [key, d] of Object.entries(DTYPES)) { | |
| const base = `onnx/model${d.suffix}.onnx`; | |
| if (!files) { if (DEFAULT_ORDER.includes(key)) available.push({key, bytes: null}); continue; } | |
| if (base in files) available.push({key, bytes: files[base] + (files[base + "_data"] ?? 0)}); | |
| } | |
| if (!available.length) available = [{key:"q8", bytes:null}]; | |
| dtSel.innerHTML = available.map(a => | |
| `<option value="${a.key}">${DTYPES[a.key].label} · ${fmt(a.bytes)}${{cpu:" · CPU only", gpu:" · GPU only", both:""}[DTYPES[a.key].dev]}</option>`).join(""); | |
| if (hasGPU) { | |
| devSel.innerHTML = `<option value="wasm">CPU</option><option value="webgpu">GPU</option>`; | |
| devSel.style.display = ""; | |
| } | |
| const pick = DEFAULT_ORDER.find(k => available.some(a => a.key === k)) | |
| ?? [...available].sort((a, b) => (a.bytes ?? 1e18) - (b.bytes ?? 1e18))[0].key; | |
| dtSel.value = pick; syncOptions(); | |
| await switchTo(); | |
| } | |
| /* ---------- load / switch (chat stays visible) ---------- */ | |
| const prog = {}; | |
| function onProgress(p) { | |
| if (p.status === "progress" && p.total) prog[p.file] = {l: p.loaded, t: p.total}; | |
| const v = Object.values(prog), L = v.reduce((s, x) => s + x.l, 0), T = v.reduce((s, x) => s + x.t, 0); | |
| if (T) { $("fill").style.width = (L / T * 100).toFixed(0) + "%"; $("dl").textContent = `Downloading ${fmt(L)} / ${fmt(T)}`; } | |
| } | |
| async function switchTo() { | |
| if (busy) return; | |
| const key = dtSel.value, a = available.find(x => x.key === key); | |
| const device = hasGPU ? devSel.value : "wasm"; | |
| if (key === current && device === currentDevice) return; | |
| if (!okOn(key, device)) { add(`${DTYPES[key].label} can't run on ${device === "webgpu" ? "GPU" : "CPU"} in this app.`, "sys"); revert(); return; } | |
| if (a?.bytes > 500e6 && !confirm(`${DTYPES[key].label} is a ${fmt(a.bytes)} download. Continue?`)) { revert(); return; } | |
| busy = true; lock(true); | |
| for (const k in prog) delete prog[k]; | |
| $("bar").style.visibility = "visible"; $("fill").style.width = "0"; $("dl").textContent = "Loading…"; | |
| $("status").textContent = `Loading ${DTYPES[key].label}…`; | |
| await paint(); | |
| try { | |
| const next = await pipeline("text-generation", MODEL, {dtype: key, device, progress_callback: onProgress}); | |
| try { await pipe?.dispose?.(); } catch {} | |
| pipe = next; | |
| add(`Rick is ready on ${DTYPES[key].label} (${device === "webgpu" ? "GPU" : "CPU"}). Try not to waste his time.`, "sys"); | |
| current = key; currentDevice = device; | |
| } catch (e) { | |
| console.error(e); | |
| add(`Couldn't load ${DTYPES[key].label}: ${e.message}`, "sys"); | |
| revert(); | |
| } | |
| $("bar").style.visibility = "hidden"; $("dl").textContent = ""; | |
| busy = false; lock(false); if (!pipe) { input.disabled = btn.disabled = true; } else input.focus(); | |
| } | |
| function syncOptions() { | |
| const dev = hasGPU ? devSel.value : "wasm"; | |
| for (const o of dtSel.options) o.disabled = !okOn(o.value, dev); | |
| if (!okOn(dtSel.value, dev)) { | |
| const k = PREF[dev].find(k => available.some(a => a.key === k)); | |
| if (k) dtSel.value = k; | |
| } | |
| } | |
| function revert() { if (current) { dtSel.value = current; if (hasGPU) devSel.value = currentDevice; } syncOptions(); } | |
| dtSel.onchange = switchTo; | |
| devSel.onchange = () => { syncOptions(); switchTo(); }; | |
| /* ---------- chat ---------- */ | |
| async function send(text) { | |
| text = text.trim(); if (!text || !pipe || busy) return; | |
| add(text, "me"); input.value = ""; busy = true; lock(true); | |
| const bubble = add("…", "rick"); | |
| history.push(text); | |
| await paint(); | |
| const eos = pipe.tokenizer.eos_token; | |
| const prompt = history.slice(-6).join(eos) + eos; // DialoGPT: turns separated by EOS | |
| let acc = ""; | |
| const streamer = new TextStreamer(pipe.tokenizer, {skip_prompt: true, skip_special_tokens: true, | |
| callback_function: t => { acc += t; bubble.textContent = acc; $("log").scrollTop = $("log").scrollHeight; }}); | |
| try { | |
| const out = await pipe(prompt, {max_new_tokens: 60, do_sample: true, top_k: 50, top_p: 0.9, | |
| temperature: 0.8, repetition_penalty: 1.2, return_full_text: false, streamer}); | |
| const reply = out[0].generated_text.split(eos)[0].trim() || "*burp* …whatever."; | |
| bubble.textContent = reply; history.push(reply); | |
| } catch (e) { bubble.textContent = "Error: " + e.message; history.pop(); } | |
| busy = false; lock(false); input.focus(); | |
| } | |
| $("f").addEventListener("submit", e => { e.preventDefault(); send(input.value); }); | |
| $("chips").addEventListener("click", e => { if (e.target.tagName === "SPAN") send(e.target.textContent); }); | |
| discover(); | |
| </script> | |
| </body> | |
| </html> | |