Quorum-Demo / index.html
JiunYi's picture
Upload index.html with huggingface_hub
2f30821 verified
Raw History Blame Contribute Delete
12 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Quorum — calibrated intent decisions from tiny open models</title>
<meta name="description" content="Pick a decision and candidate labels; Quorum returns the choice, a confidence you can gate on, and how each model voted. An ensemble of three sub-1B open models — no training, runs on a laptop." />
<meta property="og:type" content="website" />
<meta property="og:title" content="Quorum — calibrated intent decisions from tiny open models" />
<meta property="og:description" content="A jury of three sub-1B open models decides an intent and tells you how sure it is — no training, no paid API. Try it live." />
<meta property="og:url" content="https://huggingface.co/spaces/DataAgent/Quorum-Demo" />
<meta name="twitter:card" content="summary" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%234f46e5'/%3E%3Ctext x='16' y='23' font-size='20' text-anchor='middle' fill='white'%3E%E2%9A%96%3C/text%3E%3C/svg%3E" />
<style>
:root { --ink:#1a1a2e; --muted:#6b7280; --line:#e5e7eb; --accent:#4f46e5; --accent-soft:#eef0ff;
--bg:#ffffff; --card:#ffffff; --ok:#047857; --ok-bg:#ecfdf5; --warn:#b45309; --warn-bg:#fffbeb; }
@media (prefers-color-scheme: dark) {
:root { --ink:#e8e8f0; --muted:#9aa0ac; --line:#2b2f3a; --accent:#8b8cf7; --accent-soft:#20223a;
--bg:#14151c; --card:#1b1d26; --ok:#34d399; --ok-bg:#0d2b22; --warn:#fbbf24; --warn-bg:#2a2113; }
}
* { box-sizing: border-box; }
body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--ink); background: var(--bg); margin: 0; line-height: 1.55; }
.wrap { max-width: 760px; margin: 0 auto; padding: 28px 18px 40px; }
h1 { font-size: 1.55rem; margin: 0 0 6px; letter-spacing: -.01em; }
.sub { color: var(--muted); margin: 0 0 14px; }
.links a { color: var(--accent); text-decoration: none; margin-right: 14px; font-size: .9rem; }
.links a:hover { text-decoration: underline; }
.card { border: 1px solid var(--line); border-radius: 12px; padding: 16px; margin-top: 16px; background: var(--card); }
label { display:block; font-weight:600; font-size:.85rem; margin: 12px 0 4px; }
input[type=text], textarea { width:100%; padding:9px 11px; border:1px solid var(--line);
border-radius:8px; font: inherit; background: var(--bg); color: var(--ink); }
textarea { resize: vertical; }
.presets { display:flex; flex-wrap:wrap; gap:8px; margin-top:4px; }
.chip { border:1px solid var(--line); background:var(--bg); color:var(--ink); border-radius:999px;
padding:6px 13px; font-size:.85rem; cursor:pointer; }
.chip:hover { border-color: var(--accent); color: var(--accent); }
.go { margin-top: 16px; background: var(--accent); color:#fff; border:0; border-radius:8px;
padding: 11px 20px; font-weight:600; font-size:.95rem; cursor:pointer; }
.go:disabled { opacity:.55; cursor:default; }
.note { font-size:.8rem; color:var(--muted); margin-top:8px; }
.spin { display:inline-block; width:14px; height:14px; border:2px solid rgba(255,255,255,.5);
border-top-color:#fff; border-radius:50%; animation:spin .7s linear infinite; vertical-align:-2px; margin-right:7px; }
@keyframes spin { to { transform: rotate(360deg); } }
.status { font-size:.85rem; color:var(--muted); margin-top:10px; }
.badge { display:inline-block; background:var(--accent-soft); color:var(--accent); font-weight:700;
font-size:1.15rem; padding:4px 12px; border-radius:8px; }
.meta { font-size:.8rem; color:var(--muted); margin: 6px 0 2px; }
.bar { height: 20px; background: var(--accent); border-radius: 4px; }
.row { display:flex; align-items:center; gap:8px; margin: 5px 0; font-size:.9rem; }
.rowlab { width: 40%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.barwrap { flex:1; background:var(--accent-soft); border-radius:4px; }
.pct { width: 52px; text-align:right; color: var(--muted); font-variant-numeric: tabular-nums; }
.members { font-size:.85rem; margin-top:14px; }
.members b { color: var(--accent); }
.gate { margin-top:16px; border-top:1px dashed var(--line); padding-top:14px; }
.gate input[type=range] { width:100%; accent-color: var(--accent); }
.decision { margin-top:10px; padding:10px 12px; border-radius:8px; font-weight:600; font-size:.95rem; }
.decision.act { background:var(--ok-bg); color:var(--ok); }
.decision.esc { background:var(--warn-bg); color:var(--warn); }
.err { color:#dc2626; font-size:.9rem; margin-top:10px; white-space:pre-wrap; }
details { margin-top:16px; }
summary { cursor:pointer; color:var(--muted); font-size:.85rem; }
footer { margin-top:28px; padding-top:16px; border-top:1px solid var(--line); font-size:.85rem; color:var(--muted); }
footer a { color: var(--accent); text-decoration:none; }
.cta { display:inline-block; margin-top:8px; font-weight:600; }
</style>
</head>
<body>
<div class="wrap">
<h1>⚖️ Quorum</h1>
<p class="sub">Turn a plain message into a decision you can act on — the chosen label, a confidence you can set
a threshold on, and a look at how the models voted. No training, no paid API; three sub-1B open models
running on ordinary hardware.</p>
<div class="links">
<a href="https://github.com/DataAgent-Lab/Quorum" target="_blank" rel="noopener">GitHub</a>
<a href="https://huggingface.co/collections/DataAgent/quorum-6abb3255e0fab20d66add39b" target="_blank" rel="noopener">Models</a>
<a href="https://huggingface.co/DataAgent/Quorum-Reader-Qwen3-4B-Adapter" target="_blank" rel="noopener">24-shot adapter</a>
</div>
<div class="card">
<label>Try an example</label>
<div class="presets" id="presets"></div>
<label for="msg">Message</label>
<textarea id="msg" rows="2">when will my new card arrive?</textarea>
<label for="labels">Candidate labels (one per line)</label>
<textarea id="labels" rows="6">card arrival
card delivery estimate
lost or stolen card
change pin
top up by card
exchange rate</textarea>
<button class="go" id="go">Decide</button>
<div class="status" id="status"></div>
<div class="err" id="err"></div>
</div>
<div class="card" id="out" style="display:none">
<div class="meta">Top choice</div>
<div class="badge" id="toplab"></div>
<span class="meta" id="topconf"></span>
<div id="bars" style="margin-top:12px"></div>
<div class="members" id="members"></div>
<div class="gate">
<label for="tau" style="margin:0">Confidence gate — auto-act when confidence ≥ <span id="tauval">70</span>%</label>
<input type="range" id="tau" min="0" max="100" value="70" />
<div class="decision" id="decision"></div>
<div class="note">The gate is what turns a prediction into an action: auto-act above the line, escalate to a
human below it. This live demo is the zero-shot tier (uncalibrated); the self-hosted service adds
temperature calibration and a precision-<em>guaranteed</em> (conformal) gate — see the repo.</div>
</div>
</div>
<footer>
Built by <b>DataAgent</b>. <a href="https://github.com/DataAgent-Lab/Quorum" target="_blank" rel="noopener">Open source</a>,
with a small reproduction study comparing against a closed commercial API (24-shot: 0.932 vs 0.924 on Banking77).<br>
<a class="cta" href="https://github.com/DataAgent-Lab/Quorum/issues" target="_blank" rel="noopener">Want this on your own data / labels? Start a conversation →</a>
</footer>
</div>
<script>
const $ = id => document.getElementById(id);
const esc = s => String(s).replace(/[&<>"']/g, c => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c]));
const API_BASE = "https://quorum-api.idataagent.com";
const EXAMPLES = {
"Banking": { msg: "when will my new card arrive?",
labels: ["card arrival","card delivery estimate","lost or stolen card","change pin","top up by card","exchange rate"] },
"Support triage": { msg: "I was charged twice for the same order and want my money back",
labels: ["billing / double charge","refund request","order status","cancel subscription","technical bug","general question"] },
"Product feedback": { msg: "the new dashboard is gorgeous but it loads really slowly",
labels: ["praise","performance complaint","bug report","feature request","pricing concern","churn risk"] },
};
const presets = $("presets");
Object.keys(EXAMPLES).forEach(name => {
const b = document.createElement("button");
b.className = "chip"; b.textContent = name;
b.onclick = () => { $("msg").value = EXAMPLES[name].msg; $("labels").value = EXAMPLES[name].labels.join("\n"); };
presets.appendChild(b);
});
let lastConf = null;
function renderDecision() {
if (lastConf === null) return;
const tau = +$("tau").value; $("tauval").textContent = tau;
const el = $("decision"); const c = (lastConf * 100);
if (c >= tau) { el.className = "decision act"; el.textContent = `✅ Auto-act — confidence ${c.toFixed(1)}% ≥ ${tau}%`; }
else { el.className = "decision esc"; el.textContent = `⚠️ Escalate to a human — confidence ${c.toFixed(1)}% < ${tau}%`; }
}
$("tau").addEventListener("input", renderDecision);
async function decide() {
$("err").textContent = ""; $("status").textContent = ""; $("out").style.display = "none";
const base = API_BASE;
const message = $("msg").value.trim();
const labels = $("labels").value.split("\n").map(s => s.trim()).filter((s, i, a) => s && a.indexOf(s) === i);
if (!message) { $("err").textContent = "Enter a message."; return; }
if (labels.length < 2) { $("err").textContent = "Enter at least 2 labels (one per line)."; return; }
$("go").disabled = true; $("go").innerHTML = '<span class="spin"></span>Deciding…';
const warmTimer = setTimeout(() => { $("status").textContent = "Running on a small shared CPU server — a prediction takes a few seconds (longer under load)…"; }, 1200);
const ctrl = new AbortController();
const timeout = setTimeout(() => ctrl.abort(), 40000);
const t0 = performance.now();
try {
const r = await fetch(base + "/predict", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ message, labels }), signal: ctrl.signal,
});
if (!r.ok) throw new Error("The server returned HTTP " + r.status + ".");
const d = await r.json();
const secs = ((performance.now() - t0) / 1000).toFixed(1);
$("toplab").textContent = d.label;
$("topconf").textContent = ` confidence ${(d.confidence * 100).toFixed(1)}% · decided in ${secs}s`;
const entries = Object.entries(d.distribution);
$("bars").innerHTML = entries.map(([k, v]) =>
`<div class="row"><div class="rowlab" title="${esc(k)}">${esc(k)}</div>` +
`<div class="barwrap"><div class="bar" style="width:${Math.max(2, v * 100).toFixed(1)}%"></div></div>` +
`<div class="pct">${(v * 100).toFixed(1)}%</div></div>`).join("");
$("members").innerHTML = "<b>How the jury voted</b><br>" +
(d.members || []).map(m => `${esc(m.model)} → <b>${esc(m.pick)}</b>`).join("<br>");
lastConf = d.confidence; renderDecision();
$("out").style.display = "block";
} catch (e) {
$("err").textContent = (e.name === "AbortError")
? "The demo server didn't respond in time — it may be busy. Please try again in a few seconds."
: "Couldn't reach the demo server. Please try again shortly.";
} finally {
clearTimeout(warmTimer); clearTimeout(timeout);
$("status").textContent = "";
$("go").disabled = false; $("go").textContent = "Decide";
}
}
$("go").addEventListener("click", decide);
</script>
</body>
</html>