chat-with-rick / index.html
StrongTheDev's picture
Upload folder using huggingface_hub
f2436b5 verified
Raw History Blame Contribute Delete
9.8 kB
<!DOCTYPE html>
<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 &amp; 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>