JiunYi commited on
Commit
2561ea3
·
verified ·
1 Parent(s): f273e93

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +142 -45
index.html CHANGED
@@ -3,51 +3,85 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Quorum — live demo</title>
 
 
 
 
 
 
 
7
  <style>
8
- :root { --ink:#1a1a2e; --muted:#6b7280; --line:#e5e7eb; --accent:#4f46e5; --bg:#ffffff; }
 
 
 
 
 
9
  * { box-sizing: border-box; }
10
  body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
11
- color: var(--ink); background: var(--bg); margin: 0; line-height: 1.5; }
12
- .wrap { max-width: 760px; margin: 0 auto; padding: 28px 18px 60px; }
13
- h1 { font-size: 1.6rem; margin: 0 0 4px; }
14
- .sub { color: var(--muted); margin: 0 0 16px; }
15
  .links a { color: var(--accent); text-decoration: none; margin-right: 14px; font-size: .9rem; }
16
- .card { border: 1px solid var(--line); border-radius: 12px; padding: 16px; margin-top: 16px; }
17
- label { display:block; font-weight:600; font-size:.85rem; margin: 10px 0 4px; }
 
18
  input[type=text], textarea { width:100%; padding:9px 11px; border:1px solid var(--line);
19
- border-radius:8px; font: inherit; }
20
  textarea { resize: vertical; }
21
- button { margin-top: 14px; background: var(--accent); color:#fff; border:0; border-radius:8px;
22
- padding: 10px 18px; font-weight:600; cursor:pointer; }
23
- button:disabled { opacity:.5; cursor:default; }
 
 
 
 
24
  .note { font-size:.8rem; color:var(--muted); margin-top:8px; }
25
- .top { font-size:1.25rem; font-weight:700; margin: 4px 0 2px; }
 
 
 
 
 
 
26
  .bar { height: 20px; background: var(--accent); border-radius: 4px; }
27
  .row { display:flex; align-items:center; gap:8px; margin: 5px 0; font-size:.9rem; }
28
- .rowlab { width: 44%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
29
- .barwrap { flex:1; background:#f1f1f6; border-radius:4px; }
30
  .pct { width: 52px; text-align:right; color: var(--muted); font-variant-numeric: tabular-nums; }
31
- .members { font-size:.85rem; color:var(--ink); margin-top:12px; }
32
  .members b { color: var(--accent); }
33
- .err { color:#b91c1c; font-size:.9rem; margin-top:10px; white-space:pre-wrap; }
 
 
 
 
 
 
 
 
 
 
34
  </style>
35
  </head>
36
  <body>
37
  <div class="wrap">
38
- <h1>⚖️ Quorum — a jury of tiny open models</h1>
39
- <p class="sub">Type a message and a few candidate intent labels. A zero-shot ensemble of three sub-1B open
40
- models (PrismNLI-0.4B + bge-large + bge-base, geometric mean) picks the intent and tells you how sure it
41
- is — no training, no calibration.</p>
42
  <div class="links">
43
- <a href="https://github.com/DataAgent-Lab/Quorum" target="_blank">GitHub</a>
44
- <a href="https://huggingface.co/collections/DataAgent/quorum-6abb3255e0fab20d66add39b" target="_blank">Collection</a>
45
- <a href="https://huggingface.co/DataAgent/Quorum-Reader-Qwen3-4B-Adapter" target="_blank">24-shot adapter</a>
46
  </div>
47
 
48
  <div class="card">
49
- <label for="api">API URL (a running Quorum service — see the repo's <code>serve/</code>)</label>
50
- <input id="api" type="text" placeholder="https://your-quorum-api.example.com" value="https://quorum-api.idataagent.com" />
 
51
  <label for="msg">Message</label>
52
  <textarea id="msg" rows="2">when will my new card arrive?</textarea>
53
  <label for="labels">Candidate labels (one per line)</label>
@@ -57,58 +91,121 @@ lost or stolen card
57
  change pin
58
  top up by card
59
  exchange rate</textarea>
60
- <button id="go">Decide</button>
61
- <div class="note">This is the zero-shot tier. The 24-shot pipeline that edges the closed API Jev
62
- (0.932 vs 0.924) uses the gated adapter linked above. The demo points at a small shared server, so the
63
- first request can take ~15s under load (&lt;1s when idle). To run your own, self-host from the repo's
64
- <code>serve/</code> and paste its URL here (it is remembered in your browser).</div>
65
- <div id="err" class="err"></div>
66
  </div>
67
 
68
  <div class="card" id="out" style="display:none">
69
- <div class="top" id="toplab"></div>
70
- <div class="note" id="topconf"></div>
71
- <div id="bars"></div>
 
72
  <div class="members" id="members"></div>
 
 
 
 
 
 
 
 
 
73
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  </div>
75
 
76
  <script>
77
  const $ = id => document.getElementById(id);
 
78
  const api = $("api");
79
  try { const saved = localStorage.getItem("quorum_api"); if (saved) api.value = saved; } catch (e) {}
80
  api.addEventListener("change", () => { try { localStorage.setItem("quorum_api", api.value.trim()); } catch (e) {} });
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  async function decide() {
83
- $("err").textContent = ""; $("out").style.display = "none";
84
  const base = api.value.trim().replace(/\/+$/, "");
85
- if (!base) { $("err").textContent = "Enter the API URL of a running Quorum service (see serve/ in the repo)."; return; }
86
  const message = $("msg").value.trim();
87
  const labels = $("labels").value.split("\n").map(s => s.trim()).filter((s, i, a) => s && a.indexOf(s) === i);
88
  if (!message) { $("err").textContent = "Enter a message."; return; }
89
  if (labels.length < 2) { $("err").textContent = "Enter at least 2 labels (one per line)."; return; }
90
- $("go").disabled = true; $("go").textContent = "Deciding…";
 
 
 
 
 
91
  try {
92
  const r = await fetch(base + "/predict", {
93
  method: "POST", headers: { "content-type": "application/json" },
94
- body: JSON.stringify({ message, labels }),
95
  });
96
- if (!r.ok) throw new Error("API returned HTTP " + r.status + " — " + (await r.text()).slice(0, 200));
97
  const d = await r.json();
 
98
  $("toplab").textContent = d.label;
99
- $("topconf").textContent = "calibrated confidence " + (d.confidence * 100).toFixed(1) + "%";
100
  const entries = Object.entries(d.distribution);
101
  $("bars").innerHTML = entries.map(([k, v]) =>
102
- `<div class="row"><div class="rowlab" title="${k}">${k}</div>` +
103
  `<div class="barwrap"><div class="bar" style="width:${Math.max(2, v * 100).toFixed(1)}%"></div></div>` +
104
  `<div class="pct">${(v * 100).toFixed(1)}%</div></div>`).join("");
105
  $("members").innerHTML = "<b>How the jury voted</b><br>" +
106
- d.members.map(m => `${m.model} → <b>${m.pick}</b>`).join("<br>");
 
107
  $("out").style.display = "block";
108
  } catch (e) {
109
- $("err").textContent = "Could not reach the API.\n" + e.message +
110
- "\n\nCheck: the API is running and public over HTTPS, and CORS allows this page (see serve/README.md).";
 
 
 
111
  } finally {
 
 
112
  $("go").disabled = false; $("go").textContent = "Decide";
113
  }
114
  }
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Quorum — calibrated intent decisions from tiny open models</title>
7
+ <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." />
8
+ <meta property="og:type" content="website" />
9
+ <meta property="og:title" content="Quorum — calibrated intent decisions from tiny open models" />
10
+ <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." />
11
+ <meta property="og:url" content="https://huggingface.co/spaces/DataAgent/Quorum-Demo" />
12
+ <meta name="twitter:card" content="summary" />
13
+ <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" />
14
  <style>
15
+ :root { --ink:#1a1a2e; --muted:#6b7280; --line:#e5e7eb; --accent:#4f46e5; --accent-soft:#eef0ff;
16
+ --bg:#ffffff; --card:#ffffff; --ok:#047857; --ok-bg:#ecfdf5; --warn:#b45309; --warn-bg:#fffbeb; }
17
+ @media (prefers-color-scheme: dark) {
18
+ :root { --ink:#e8e8f0; --muted:#9aa0ac; --line:#2b2f3a; --accent:#8b8cf7; --accent-soft:#20223a;
19
+ --bg:#14151c; --card:#1b1d26; --ok:#34d399; --ok-bg:#0d2b22; --warn:#fbbf24; --warn-bg:#2a2113; }
20
+ }
21
  * { box-sizing: border-box; }
22
  body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
23
+ color: var(--ink); background: var(--bg); margin: 0; line-height: 1.55; }
24
+ .wrap { max-width: 760px; margin: 0 auto; padding: 28px 18px 40px; }
25
+ h1 { font-size: 1.55rem; margin: 0 0 6px; letter-spacing: -.01em; }
26
+ .sub { color: var(--muted); margin: 0 0 14px; }
27
  .links a { color: var(--accent); text-decoration: none; margin-right: 14px; font-size: .9rem; }
28
+ .links a:hover { text-decoration: underline; }
29
+ .card { border: 1px solid var(--line); border-radius: 12px; padding: 16px; margin-top: 16px; background: var(--card); }
30
+ label { display:block; font-weight:600; font-size:.85rem; margin: 12px 0 4px; }
31
  input[type=text], textarea { width:100%; padding:9px 11px; border:1px solid var(--line);
32
+ border-radius:8px; font: inherit; background: var(--bg); color: var(--ink); }
33
  textarea { resize: vertical; }
34
+ .presets { display:flex; flex-wrap:wrap; gap:8px; margin-top:4px; }
35
+ .chip { border:1px solid var(--line); background:var(--bg); color:var(--ink); border-radius:999px;
36
+ padding:6px 13px; font-size:.85rem; cursor:pointer; }
37
+ .chip:hover { border-color: var(--accent); color: var(--accent); }
38
+ .go { margin-top: 16px; background: var(--accent); color:#fff; border:0; border-radius:8px;
39
+ padding: 11px 20px; font-weight:600; font-size:.95rem; cursor:pointer; }
40
+ .go:disabled { opacity:.55; cursor:default; }
41
  .note { font-size:.8rem; color:var(--muted); margin-top:8px; }
42
+ .spin { display:inline-block; width:14px; height:14px; border:2px solid rgba(255,255,255,.5);
43
+ border-top-color:#fff; border-radius:50%; animation:spin .7s linear infinite; vertical-align:-2px; margin-right:7px; }
44
+ @keyframes spin { to { transform: rotate(360deg); } }
45
+ .status { font-size:.85rem; color:var(--muted); margin-top:10px; }
46
+ .badge { display:inline-block; background:var(--accent-soft); color:var(--accent); font-weight:700;
47
+ font-size:1.15rem; padding:4px 12px; border-radius:8px; }
48
+ .meta { font-size:.8rem; color:var(--muted); margin: 6px 0 2px; }
49
  .bar { height: 20px; background: var(--accent); border-radius: 4px; }
50
  .row { display:flex; align-items:center; gap:8px; margin: 5px 0; font-size:.9rem; }
51
+ .rowlab { width: 40%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
52
+ .barwrap { flex:1; background:var(--accent-soft); border-radius:4px; }
53
  .pct { width: 52px; text-align:right; color: var(--muted); font-variant-numeric: tabular-nums; }
54
+ .members { font-size:.85rem; margin-top:14px; }
55
  .members b { color: var(--accent); }
56
+ .gate { margin-top:16px; border-top:1px dashed var(--line); padding-top:14px; }
57
+ .gate input[type=range] { width:100%; accent-color: var(--accent); }
58
+ .decision { margin-top:10px; padding:10px 12px; border-radius:8px; font-weight:600; font-size:.95rem; }
59
+ .decision.act { background:var(--ok-bg); color:var(--ok); }
60
+ .decision.esc { background:var(--warn-bg); color:var(--warn); }
61
+ .err { color:#dc2626; font-size:.9rem; margin-top:10px; white-space:pre-wrap; }
62
+ details { margin-top:16px; }
63
+ summary { cursor:pointer; color:var(--muted); font-size:.85rem; }
64
+ footer { margin-top:28px; padding-top:16px; border-top:1px solid var(--line); font-size:.85rem; color:var(--muted); }
65
+ footer a { color: var(--accent); text-decoration:none; }
66
+ .cta { display:inline-block; margin-top:8px; font-weight:600; }
67
  </style>
68
  </head>
69
  <body>
70
  <div class="wrap">
71
+ <h1>⚖️ Quorum</h1>
72
+ <p class="sub">Turn a plain message into a decision you can act on — the chosen label, a confidence you can set
73
+ a threshold on, and a look at how the models voted. No training, no paid API; three sub-1B open models
74
+ running on ordinary hardware.</p>
75
  <div class="links">
76
+ <a href="https://github.com/DataAgent-Lab/Quorum" target="_blank" rel="noopener">GitHub</a>
77
+ <a href="https://huggingface.co/collections/DataAgent/quorum-6abb3255e0fab20d66add39b" target="_blank" rel="noopener">Models</a>
78
+ <a href="https://huggingface.co/DataAgent/Quorum-Reader-Qwen3-4B-Adapter" target="_blank" rel="noopener">24-shot adapter</a>
79
  </div>
80
 
81
  <div class="card">
82
+ <label>Try an example</label>
83
+ <div class="presets" id="presets"></div>
84
+
85
  <label for="msg">Message</label>
86
  <textarea id="msg" rows="2">when will my new card arrive?</textarea>
87
  <label for="labels">Candidate labels (one per line)</label>
 
91
  change pin
92
  top up by card
93
  exchange rate</textarea>
94
+ <button class="go" id="go">Decide</button>
95
+ <div class="status" id="status"></div>
96
+ <div class="err" id="err"></div>
 
 
 
97
  </div>
98
 
99
  <div class="card" id="out" style="display:none">
100
+ <div class="meta">Top choice</div>
101
+ <div class="badge" id="toplab"></div>
102
+ <span class="meta" id="topconf"></span>
103
+ <div id="bars" style="margin-top:12px"></div>
104
  <div class="members" id="members"></div>
105
+
106
+ <div class="gate">
107
+ <label for="tau" style="margin:0">Confidence gate — auto-act when confidence ≥ <span id="tauval">70</span>%</label>
108
+ <input type="range" id="tau" min="0" max="100" value="70" />
109
+ <div class="decision" id="decision"></div>
110
+ <div class="note">The gate is what turns a prediction into an action: auto-act above the line, escalate to a
111
+ human below it. This live demo is the zero-shot tier (uncalibrated); the self-hosted service adds
112
+ temperature calibration and a precision-<em>guaranteed</em> (conformal) gate — see the repo.</div>
113
+ </div>
114
  </div>
115
+
116
+ <details>
117
+ <summary>Advanced — run against your own Quorum server</summary>
118
+ <div class="card" style="margin-top:10px">
119
+ <label for="api">API URL</label>
120
+ <input id="api" type="text" placeholder="https://your-quorum-api.example.com" value="https://quorum-api.idataagent.com" />
121
+ <div class="note">The demo talks to a small shared server by default. To use your own, self-host from the
122
+ repo's <code>serve/</code> (any CPU box), expose it over HTTPS with CORS allowing this page, and paste
123
+ its URL here — it is remembered in your browser.</div>
124
+ </div>
125
+ </details>
126
+
127
+ <footer>
128
+ Built by <b>DataAgent</b>. <a href="https://github.com/DataAgent-Lab/Quorum" target="_blank" rel="noopener">Open source</a>,
129
+ with a small reproduction study comparing against a closed commercial API (24-shot: 0.932 vs 0.924 on Banking77).<br>
130
+ <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>
131
+ </footer>
132
  </div>
133
 
134
  <script>
135
  const $ = id => document.getElementById(id);
136
+ const esc = s => String(s).replace(/[&<>"']/g, c => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c]));
137
  const api = $("api");
138
  try { const saved = localStorage.getItem("quorum_api"); if (saved) api.value = saved; } catch (e) {}
139
  api.addEventListener("change", () => { try { localStorage.setItem("quorum_api", api.value.trim()); } catch (e) {} });
140
 
141
+ const EXAMPLES = {
142
+ "Banking": { msg: "when will my new card arrive?",
143
+ labels: ["card arrival","card delivery estimate","lost or stolen card","change pin","top up by card","exchange rate"] },
144
+ "Support triage": { msg: "I was charged twice for the same order and want my money back",
145
+ labels: ["billing / double charge","refund request","order status","cancel subscription","technical bug","general question"] },
146
+ "Product feedback": { msg: "the new dashboard is gorgeous but it loads really slowly",
147
+ labels: ["praise","performance complaint","bug report","feature request","pricing concern","churn risk"] },
148
+ };
149
+ const presets = $("presets");
150
+ Object.keys(EXAMPLES).forEach(name => {
151
+ const b = document.createElement("button");
152
+ b.className = "chip"; b.textContent = name;
153
+ b.onclick = () => { $("msg").value = EXAMPLES[name].msg; $("labels").value = EXAMPLES[name].labels.join("\n"); };
154
+ presets.appendChild(b);
155
+ });
156
+
157
+ let lastConf = null;
158
+ function renderDecision() {
159
+ if (lastConf === null) return;
160
+ const tau = +$("tau").value; $("tauval").textContent = tau;
161
+ const el = $("decision"); const c = (lastConf * 100);
162
+ if (c >= tau) { el.className = "decision act"; el.textContent = `✅ Auto-act — confidence ${c.toFixed(1)}% ≥ ${tau}%`; }
163
+ else { el.className = "decision esc"; el.textContent = `⚠️ Escalate to a human — confidence ${c.toFixed(1)}% < ${tau}%`; }
164
+ }
165
+ $("tau").addEventListener("input", renderDecision);
166
+
167
  async function decide() {
168
+ $("err").textContent = ""; $("status").textContent = ""; $("out").style.display = "none";
169
  const base = api.value.trim().replace(/\/+$/, "");
170
+ if (!base) { $("err").textContent = "Set an API URL under “Advanced”."; return; }
171
  const message = $("msg").value.trim();
172
  const labels = $("labels").value.split("\n").map(s => s.trim()).filter((s, i, a) => s && a.indexOf(s) === i);
173
  if (!message) { $("err").textContent = "Enter a message."; return; }
174
  if (labels.length < 2) { $("err").textContent = "Enter at least 2 labels (one per line)."; return; }
175
+
176
+ $("go").disabled = true; $("go").innerHTML = '<span class="spin"></span>Deciding…';
177
+ const warmTimer = setTimeout(() => { $("status").textContent = "Waking the shared demo server — the first request can take ~15s…"; }, 1500);
178
+ const ctrl = new AbortController();
179
+ const timeout = setTimeout(() => ctrl.abort(), 40000);
180
+ const t0 = performance.now();
181
  try {
182
  const r = await fetch(base + "/predict", {
183
  method: "POST", headers: { "content-type": "application/json" },
184
+ body: JSON.stringify({ message, labels }), signal: ctrl.signal,
185
  });
186
+ if (!r.ok) throw new Error("The server returned HTTP " + r.status + ".");
187
  const d = await r.json();
188
+ const secs = ((performance.now() - t0) / 1000).toFixed(1);
189
  $("toplab").textContent = d.label;
190
+ $("topconf").textContent = ` confidence ${(d.confidence * 100).toFixed(1)}% · decided in ${secs}s`;
191
  const entries = Object.entries(d.distribution);
192
  $("bars").innerHTML = entries.map(([k, v]) =>
193
+ `<div class="row"><div class="rowlab" title="${esc(k)}">${esc(k)}</div>` +
194
  `<div class="barwrap"><div class="bar" style="width:${Math.max(2, v * 100).toFixed(1)}%"></div></div>` +
195
  `<div class="pct">${(v * 100).toFixed(1)}%</div></div>`).join("");
196
  $("members").innerHTML = "<b>How the jury voted</b><br>" +
197
+ (d.members || []).map(m => `${esc(m.model)} → <b>${esc(m.pick)}</b>`).join("<br>");
198
+ lastConf = d.confidence; renderDecision();
199
  $("out").style.display = "block";
200
  } catch (e) {
201
+ const msg = (e.name === "AbortError")
202
+ ? "The demo server didn't respond in time — it may be waking up or busy. Please try again in a few seconds."
203
+ : "Couldn't reach the demo server. Please try again shortly" +
204
+ (api.value.includes("idataagent.com") ? "." : ", or check your API URL under “Advanced”.");
205
+ $("err").textContent = msg;
206
  } finally {
207
+ clearTimeout(warmTimer); clearTimeout(timeout);
208
+ $("status").textContent = "";
209
  $("go").disabled = false; $("go").textContent = "Decide";
210
  }
211
  }