Spaces:
Running
Running
Phase 1.0b: redesigned demo (Quorum main c013b4e)
Browse files- README.md +18 -14
- index.html +505 -171
- samples.json +0 -0
README.md
CHANGED
|
@@ -1,26 +1,30 @@
|
|
| 1 |
---
|
| 2 |
title: Quorum
|
| 3 |
emoji: ⚖️
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: gray
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
-
# Quorum —
|
| 12 |
|
| 13 |
-
A jury of tiny open models
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
**
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
**Honest note.** This is the zero-shot tier — cheap and calibrated, but *not* open zero-shot state-of-the-art.
|
| 25 |
-
The 24-shot pipeline that edges the closed API Jev (0.932 vs 0.924 on Banking77) uses a gated LoRA adapter,
|
| 26 |
-
`DataAgent/Quorum-Reader-Qwen3-4B-Adapter`.
|
|
|
|
| 1 |
---
|
| 2 |
title: Quorum
|
| 3 |
emoji: ⚖️
|
| 4 |
+
colorFrom: red
|
| 5 |
colorTo: gray
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Quorum — ask the jury
|
| 12 |
|
| 13 |
+
A jury of three tiny open models reads a message, each casts a vote, and Quorum returns the verdict with a confidence
|
| 14 |
+
you can act on. Every example on the page is a **real test sentence** from one of seven public intent benchmarks
|
| 15 |
+
(Banking77, CLINC150, HWU64, MASSIVE, MTOP, SNIPS, Bitext), so you can see when the jury gets it right — and when it
|
| 16 |
+
doesn't. You can also type your own sentence and labels.
|
| 17 |
|
| 18 |
+
- The page is static (`index.html` + `samples.json`) and calls a self-hosted Quorum API. Benchmark examples are
|
| 19 |
+
answered instantly from the API's cache; your own sentences are computed live (the first one after a quiet spell
|
| 20 |
+
takes a minute or two while the models wake up). Sentences you type are kept on the demo server so repeats answer
|
| 21 |
+
instantly.
|
| 22 |
+
- Run your own API: see [`serve/`](https://github.com/DataAgent-Lab/Quorum/tree/main/serve) and set `DEFAULT_API` in
|
| 23 |
+
your copy of `index.html` (when the page is served from localhost, `?api=https://your-api.example` also works).
|
| 24 |
+
- `samples.json` is generated from the per-item benchmark predictions: `python -m serve.export_demo_samples`.
|
| 25 |
+
- Code + study: https://github.com/DataAgent-Lab/Quorum ·
|
| 26 |
+
Collection: https://huggingface.co/collections/DataAgent/quorum-6abb3255e0fab20d66add39b
|
| 27 |
|
| 28 |
+
**Honest note.** This is the zero-shot tier — cheap and calibrated, but *not* open zero-shot state-of-the-art: the
|
| 29 |
+
closed API Jev (an independent reproduction, ~0.801 on Banking77) still leads zero-shot. The 24-shot pipeline that
|
| 30 |
+
edges it (0.932 vs 0.924 on Banking77) uses a gated LoRA adapter, `DataAgent/Quorum-Reader-Qwen3-4B-Adapter`.
|
|
|
|
|
|
|
|
|
index.html
CHANGED
|
@@ -3,197 +3,531 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
-
<title>Quorum —
|
| 7 |
-
<meta name="description" content="
|
| 8 |
<meta property="og:type" content="website" />
|
| 9 |
-
<meta property="og:title" content="Quorum —
|
| 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
|
| 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='%
|
|
|
|
|
|
|
|
|
|
| 14 |
<style>
|
| 15 |
-
:root {
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
@media (prefers-color-scheme: dark) {
|
| 18 |
-
:root
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
* { box-sizing: border-box; }
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
.
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
.
|
| 36 |
-
|
| 37 |
-
.
|
| 38 |
-
.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
.
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
.
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
.
|
| 49 |
-
.
|
| 50 |
-
.
|
| 51 |
-
.
|
| 52 |
-
|
| 53 |
-
.
|
| 54 |
-
.
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
.
|
| 58 |
-
.
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
.
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
</style>
|
| 68 |
</head>
|
| 69 |
<body>
|
| 70 |
<div class="wrap">
|
| 71 |
-
|
| 72 |
-
<
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
<
|
| 79 |
-
</
|
| 80 |
-
|
| 81 |
-
<
|
| 82 |
-
<
|
| 83 |
-
<
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
<
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
<footer>
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
<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>
|
| 120 |
</footer>
|
| 121 |
</div>
|
| 122 |
|
|
|
|
| 123 |
<script>
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
};
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
}
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
}
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
</script>
|
| 198 |
</body>
|
| 199 |
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Quorum — ask the jury</title>
|
| 7 |
+
<meta name="description" content="Three tiny open models read a message, each casts a vote, and Quorum returns a verdict with a confidence you can act on. Try it on real benchmark sentences." />
|
| 8 |
<meta property="og:type" content="website" />
|
| 9 |
+
<meta property="og:title" content="Quorum — ask the jury" />
|
| 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 on real benchmark sentences." />
|
| 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='%23b8391f'/%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 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 15 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 16 |
+
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Hanken+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
| 17 |
<style>
|
| 18 |
+
:root {
|
| 19 |
+
--paper:#f4efe5; --paper-2:#ece4d5; --card:#fbf8f1; --ink:#1c1813; --ink-2:#4b443a; --muted:#7c7367;
|
| 20 |
+
--line:#ddd3c3; --line-2:#cbbfab; --seal:#b8391f; --seal-2:#d9573a; --seal-soft:#f5ddd3;
|
| 21 |
+
--ok:#2c6a3c; --ok-soft:#e1efe1; --no:#a0341f; --no-soft:#f6e1da;
|
| 22 |
+
--shadow: 0 1px 0 rgba(28,24,19,.04), 0 10px 30px -18px rgba(28,24,19,.35);
|
| 23 |
+
--serif: "Instrument Serif", "Iowan Old Style", Georgia, serif;
|
| 24 |
+
--sans: "Hanken Grotesk", ui-sans-serif, system-ui, sans-serif;
|
| 25 |
+
--mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
|
| 26 |
+
--ease: cubic-bezier(.2,.8,.2,1);
|
| 27 |
+
}
|
| 28 |
@media (prefers-color-scheme: dark) {
|
| 29 |
+
:root:not([data-theme="light"]) {
|
| 30 |
+
--paper:#13100c; --paper-2:#1a1611; --card:#1c1813; --ink:#f0e8da; --ink-2:#cfc4b3; --muted:#9b9081;
|
| 31 |
+
--line:#2e2821; --line-2:#3c342b; --seal:#ee6a49; --seal-2:#ff8a6a; --seal-soft:#3a1d15;
|
| 32 |
+
--ok:#86c897; --ok-soft:#16271b; --no:#f2907a; --no-soft:#35190f;
|
| 33 |
+
--shadow: 0 1px 0 rgba(0,0,0,.4), 0 16px 40px -20px rgba(0,0,0,.8);
|
| 34 |
+
}
|
| 35 |
}
|
| 36 |
* { box-sizing: border-box; }
|
| 37 |
+
html { -webkit-text-size-adjust: 100%; }
|
| 38 |
+
body { margin:0; font-family:var(--sans); color:var(--ink); background:var(--paper); line-height:1.55;
|
| 39 |
+
font-size:15.5px; min-height:100vh; }
|
| 40 |
+
body::before { /* paper grain */
|
| 41 |
+
content:""; position:fixed; inset:0; pointer-events:none; z-index:0; opacity:.5;
|
| 42 |
+
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
| 43 |
+
}
|
| 44 |
+
.wrap { position:relative; z-index:1; max-width:1120px; margin:0 auto; padding:22px 20px 56px; }
|
| 45 |
+
a { color:inherit; }
|
| 46 |
+
|
| 47 |
+
/* masthead */
|
| 48 |
+
.mast { display:flex; justify-content:space-between; align-items:baseline; gap:16px; flex-wrap:wrap;
|
| 49 |
+
border-bottom:1px solid var(--line); padding-bottom:14px; }
|
| 50 |
+
.brand { font:400 1.65rem/1 var(--serif); letter-spacing:.01em; }
|
| 51 |
+
.brand small { font:500 .78rem var(--mono); color:var(--muted); margin-left:10px; letter-spacing:.04em; }
|
| 52 |
+
.mast nav { display:flex; gap:18px; font-size:.88rem; }
|
| 53 |
+
.mast nav a { text-decoration:none; color:var(--ink-2); border-bottom:1px solid transparent; }
|
| 54 |
+
.mast nav a:hover { border-color:var(--ink-2); }
|
| 55 |
+
|
| 56 |
+
/* hero */
|
| 57 |
+
.hero { display:grid; grid-template-columns: 1.15fr .85fr; gap:28px; align-items:end; margin:34px 0 26px; }
|
| 58 |
+
.hero h1 { font:400 clamp(2.6rem, 6vw, 4.6rem)/.95 var(--serif); margin:0; letter-spacing:-.01em; }
|
| 59 |
+
.hero h1 em { color:var(--seal); }
|
| 60 |
+
.hero p { margin:0; color:var(--ink-2); font-size:1rem; max-width:44ch; }
|
| 61 |
+
|
| 62 |
+
/* court: case | verdict */
|
| 63 |
+
.court { display:grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap:22px; align-items:start; }
|
| 64 |
+
.card { background:var(--card); border:1px solid var(--line); border-radius:16px; box-shadow:var(--shadow); }
|
| 65 |
+
.case { padding:22px 22px 20px; }
|
| 66 |
+
.step { display:flex; align-items:baseline; gap:10px; font:500 .72rem/1 var(--mono); letter-spacing:.12em;
|
| 67 |
+
text-transform:uppercase; color:var(--muted); margin:2px 0 12px; }
|
| 68 |
+
.step b { color:var(--seal); font-weight:500; }
|
| 69 |
+
.step + * + .step, .block + .step { margin-top:22px; }
|
| 70 |
+
|
| 71 |
+
/* domain chips */
|
| 72 |
+
.domains { display:flex; flex-wrap:wrap; gap:8px; }
|
| 73 |
+
.chip { text-align:left; border:1px solid var(--line); background:var(--paper); color:var(--ink);
|
| 74 |
+
border-radius:12px; padding:9px 11px 8px; cursor:pointer; font:inherit;
|
| 75 |
+
transition: border-color .2s, background .2s, transform .2s var(--ease); }
|
| 76 |
+
.chip:hover { border-color:var(--line-2); transform:translateY(-1px); }
|
| 77 |
+
.chip .t { display:block; font-weight:600; font-size:.88rem; line-height:1.2; white-space:nowrap; }
|
| 78 |
+
.chip .s { display:block; font:400 .7rem var(--mono); color:var(--muted); margin-top:3px; white-space:nowrap; }
|
| 79 |
+
.chip[aria-pressed="true"] { background:var(--ink); color:var(--paper); border-color:var(--ink); }
|
| 80 |
+
.chip[aria-pressed="true"] .s { color:color-mix(in srgb, var(--paper) 70%, transparent); }
|
| 81 |
+
.chip.own .t::before { content:"✎ "; }
|
| 82 |
+
.chip:focus-visible, .btn:focus-visible, .linkbtn:focus-visible, textarea:focus-visible, input:focus-visible {
|
| 83 |
+
outline:2px solid var(--seal); outline-offset:2px; }
|
| 84 |
+
|
| 85 |
+
/* message */
|
| 86 |
+
.fieldhead { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:7px; }
|
| 87 |
+
.tag { font:500 .7rem/1 var(--mono); letter-spacing:.04em; color:var(--ok); background:var(--ok-soft);
|
| 88 |
+
border-radius:999px; padding:5px 9px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
| 89 |
+
.tag.edited { color:var(--muted); background:var(--paper-2); }
|
| 90 |
+
.linkbtn { background:none; border:0; padding:4px 2px; color:var(--ink-2); font:500 .82rem var(--sans);
|
| 91 |
+
cursor:pointer; text-decoration:underline; text-decoration-color:var(--line-2); text-underline-offset:3px; }
|
| 92 |
+
.linkbtn:hover { color:var(--seal); text-decoration-color:var(--seal); }
|
| 93 |
+
textarea { width:100%; border:1px solid var(--line); border-radius:12px; background:var(--paper); color:var(--ink);
|
| 94 |
+
font: 400 1.12rem/1.45 var(--serif); padding:12px 14px; resize:vertical; min-height:74px; }
|
| 95 |
+
textarea#own-labels { font:400 .92rem/1.5 var(--sans); min-height:140px; }
|
| 96 |
+
|
| 97 |
+
/* labels summary */
|
| 98 |
+
.labels-sum { display:flex; justify-content:space-between; align-items:center; gap:12px; border:1px solid var(--line);
|
| 99 |
+
border-radius:12px; background:var(--paper); padding:11px 14px; }
|
| 100 |
+
.labels-sum .n { font:400 1.35rem/1 var(--serif); }
|
| 101 |
+
.labels-sum .what { font-size:.88rem; color:var(--ink-2); }
|
| 102 |
+
.labels-list { margin-top:8px; max-height:220px; overflow:auto; border:1px solid var(--line); border-radius:12px;
|
| 103 |
+
padding:4px 0; background:var(--paper); }
|
| 104 |
+
.labels-list li { list-style:none; padding:6px 14px; border-bottom:1px dashed var(--line); }
|
| 105 |
+
.labels-list li:last-child { border-bottom:0; }
|
| 106 |
+
.labels-list .nm { font-weight:600; font-size:.88rem; }
|
| 107 |
+
.labels-list .ds { display:block; font-size:.76rem; color:var(--muted); }
|
| 108 |
+
.hint { font-size:.8rem; color:var(--muted); margin-top:6px; }
|
| 109 |
+
.hint.warn { color:var(--no); }
|
| 110 |
+
|
| 111 |
+
.actions { display:flex; align-items:center; gap:14px; margin-top:22px; flex-wrap:wrap; }
|
| 112 |
+
.btn { position:relative; border:0; border-radius:12px; padding:13px 22px 12px; cursor:pointer;
|
| 113 |
+
background:var(--ink); color:var(--paper); font:600 .98rem var(--sans); letter-spacing:.01em;
|
| 114 |
+
transition: transform .2s var(--ease), background .2s; }
|
| 115 |
+
.btn:hover { transform:translateY(-1px); background:var(--seal); }
|
| 116 |
+
.btn:disabled { opacity:.45; cursor:not-allowed; transform:none; background:var(--ink); }
|
| 117 |
+
.btn .arrow { display:inline-block; transition:transform .25s var(--ease); margin-left:6px; }
|
| 118 |
+
.btn:hover .arrow { transform:translateX(3px); }
|
| 119 |
+
.privacy { font-size:.74rem; color:var(--muted); max-width:34ch; }
|
| 120 |
+
|
| 121 |
+
/* verdict panel */
|
| 122 |
+
.verdict { padding:22px; min-height:520px; position:sticky; top:18px; overflow:hidden; }
|
| 123 |
+
.empty { height:100%; min-height:470px; display:grid; place-items:center; text-align:center; color:var(--muted); }
|
| 124 |
+
.empty .glyph { font:400 4.2rem/1 var(--serif); color:var(--line-2); }
|
| 125 |
+
.empty p { max-width:30ch; margin:10px auto 0; font-size:.92rem; }
|
| 126 |
+
|
| 127 |
+
.jurors { display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:8px; perspective:900px; }
|
| 128 |
+
.juror { border:1px solid var(--line); border-radius:12px; padding:10px 10px 9px; background:var(--paper);
|
| 129 |
+
min-height:86px; transform-origin:50% 0; }
|
| 130 |
+
.juror .who { font:500 .66rem/1.2 var(--mono); letter-spacing:.05em; color:var(--muted); text-transform:uppercase; }
|
| 131 |
+
.juror .role { font-size:.72rem; color:var(--muted); }
|
| 132 |
+
.juror .vote { margin-top:7px; font-weight:600; font-size:.86rem; line-height:1.25; }
|
| 133 |
+
.juror .agree { font:500 .66rem var(--mono); margin-top:4px; letter-spacing:.04em; }
|
| 134 |
+
.juror .agree.y { color:var(--ok); } .juror .agree.n { color:var(--no); }
|
| 135 |
+
.juror.thinking .vote { color:var(--muted); }
|
| 136 |
+
.dots span { display:inline-block; width:5px; height:5px; border-radius:50%; background:var(--muted); margin-right:3px;
|
| 137 |
+
animation: blink 1.2s infinite both; }
|
| 138 |
+
.dots span:nth-child(2) { animation-delay:.2s } .dots span:nth-child(3) { animation-delay:.4s }
|
| 139 |
+
@keyframes blink { 0%,80%,100% { opacity:.2 } 40% { opacity:1 } }
|
| 140 |
+
|
| 141 |
+
.ruling { display:grid; grid-template-columns: minmax(0,1fr) auto; gap:16px; align-items:center;
|
| 142 |
+
margin:22px 0 6px; padding:18px 0 16px; border-top:1px solid var(--line); border-bottom:1px solid var(--line); }
|
| 143 |
+
.eyebrow { font:500 .7rem/1 var(--mono); letter-spacing:.14em; text-transform:uppercase; color:var(--seal); }
|
| 144 |
+
.ruling .name { font:400 clamp(2rem, 4.2vw, 2.9rem)/1 var(--serif); margin:8px 0 6px; overflow-wrap:anywhere; }
|
| 145 |
+
.ruling .desc { font-size:.86rem; color:var(--ink-2); }
|
| 146 |
+
.seal { width:136px; height:136px; position:relative; flex:none; }
|
| 147 |
+
.seal svg { position:absolute; inset:0; }
|
| 148 |
+
.seal .num { position:absolute; inset:0; display:grid; place-items:center; text-align:center; color:var(--seal); }
|
| 149 |
+
.seal .num b { display:block; font:500 1.18rem/1 var(--mono); letter-spacing:-.03em; }
|
| 150 |
+
.seal .num i { display:block; font:500 .5rem/1 var(--mono); letter-spacing:.18em; text-transform:uppercase;
|
| 151 |
+
font-style:normal; margin-top:5px; opacity:.85; }
|
| 152 |
+
|
| 153 |
+
.truth { display:flex; gap:10px; align-items:flex-start; border-radius:12px; padding:11px 13px; margin-top:14px;
|
| 154 |
+
font-size:.9rem; }
|
| 155 |
+
.truth .ic { font:600 1rem/1.3 var(--mono); }
|
| 156 |
+
.truth.ok { background:var(--ok-soft); color:var(--ok); }
|
| 157 |
+
.truth.no { background:var(--no-soft); color:var(--no); }
|
| 158 |
+
.truth.na { background:var(--paper-2); color:var(--ink-2); }
|
| 159 |
+
.truth small { display:block; color:var(--ink-2); opacity:.85; margin-top:2px; font-size:.78rem; }
|
| 160 |
+
|
| 161 |
+
.bars { margin-top:16px; }
|
| 162 |
+
.bar-row { display:grid; grid-template-columns: minmax(0, 1fr) 54px; gap:4px 10px; align-items:center; padding:6px 0; }
|
| 163 |
+
.bar-row .lab { font-size:.86rem; font-weight:600; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 164 |
+
.bar-row .lab span { font-weight:400; color:var(--muted); font-size:.74rem; margin-left:6px; }
|
| 165 |
+
.bar-row .pct { font:500 .8rem var(--mono); text-align:right; color:var(--ink-2); grid-row: span 2; }
|
| 166 |
+
.track { height:6px; background:var(--paper-2); border-radius:99px; overflow:hidden; }
|
| 167 |
+
.fill { height:100%; width:0; background:var(--line-2); border-radius:99px; transition: width .9s var(--ease); }
|
| 168 |
+
.bar-row.top .fill { background:var(--seal); }
|
| 169 |
+
.bar-row.top .lab { color:var(--seal); }
|
| 170 |
+
.more { margin-top:4px; }
|
| 171 |
+
|
| 172 |
+
.gate { margin-top:18px; padding-top:14px; border-top:1px dashed var(--line); }
|
| 173 |
+
.gate-head { display:flex; justify-content:space-between; font-size:.84rem; color:var(--ink-2); }
|
| 174 |
+
.gate-head b { font-family:var(--mono); font-weight:500; }
|
| 175 |
+
input[type=range] { width:100%; accent-color:var(--seal); margin:10px 0 6px; }
|
| 176 |
+
.decision { border-radius:10px; padding:10px 12px; font-weight:600; font-size:.9rem; }
|
| 177 |
+
.decision.act { background:var(--ok-soft); color:var(--ok); }
|
| 178 |
+
.decision.esc { background:var(--no-soft); color:var(--no); }
|
| 179 |
+
.footnote { margin-top:14px; font:400 .72rem/1.5 var(--mono); color:var(--muted); display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; }
|
| 180 |
+
.err { margin-top:14px; color:var(--no); font-size:.95rem; }
|
| 181 |
+
.err .btn { margin-top:14px; }
|
| 182 |
+
.waking { margin-top:18px; border:1px dashed var(--line-2); border-radius:12px; padding:14px 16px; }
|
| 183 |
+
.waking b { display:block; font:400 1.35rem/1.15 var(--serif); }
|
| 184 |
+
.waking p { margin:6px 0 0; font-size:.86rem; color:var(--ink-2); }
|
| 185 |
+
.waking .clock { font:500 .8rem var(--mono); color:var(--seal); margin-top:8px; }
|
| 186 |
+
|
| 187 |
+
/* reveal choreography */
|
| 188 |
+
@media (prefers-reduced-motion: no-preference) {
|
| 189 |
+
.reveal .juror { animation: flip .55s var(--ease) both; }
|
| 190 |
+
.reveal .juror:nth-child(2) { animation-delay:.12s } .reveal .juror:nth-child(3) { animation-delay:.24s }
|
| 191 |
+
.reveal .ruling .eyebrow, .reveal .ruling .name, .reveal .ruling .desc { animation: rise .6s var(--ease) both; }
|
| 192 |
+
.reveal .ruling .eyebrow { animation-delay:.42s } .reveal .ruling .name { animation-delay:.5s }
|
| 193 |
+
.reveal .ruling .desc { animation-delay:.6s }
|
| 194 |
+
.reveal .seal { animation: stamp .55s cubic-bezier(.3,1.6,.5,1) .62s both; }
|
| 195 |
+
.reveal .truth { animation: rise .5s var(--ease) 1.05s both; }
|
| 196 |
+
.reveal .bars, .reveal .gate, .reveal .footnote { animation: fade .6s var(--ease) .9s both; }
|
| 197 |
+
.verdict.deliberating .juror { animation: pulse 1.4s ease-in-out infinite; }
|
| 198 |
+
}
|
| 199 |
+
@keyframes flip { from { transform: rotateX(-88deg); opacity:0 } to { transform:none; opacity:1 } }
|
| 200 |
+
@keyframes rise { from { transform: translateY(14px); opacity:0 } to { transform:none; opacity:1 } }
|
| 201 |
+
@keyframes fade { from { opacity:0 } to { opacity:1 } }
|
| 202 |
+
@keyframes stamp { 0% { transform: scale(1.9) rotate(-18deg); opacity:0 } 60% { opacity:1 }
|
| 203 |
+
100% { transform: scale(1) rotate(-8deg); opacity:1 } }
|
| 204 |
+
@keyframes pulse { 0%,100% { opacity:1 } 50% { opacity:.55 } }
|
| 205 |
+
.seal { transform: rotate(-8deg); }
|
| 206 |
+
|
| 207 |
+
footer { margin-top:44px; padding-top:18px; border-top:1px solid var(--line); font-size:.82rem; color:var(--muted);
|
| 208 |
+
display:flex; justify-content:space-between; gap:16px; flex-wrap:wrap; }
|
| 209 |
+
footer a { color:var(--ink-2); }
|
| 210 |
+
|
| 211 |
+
@media (max-width: 900px) {
|
| 212 |
+
.hero { grid-template-columns:1fr; gap:14px; margin-top:26px; }
|
| 213 |
+
.court { grid-template-columns:1fr; }
|
| 214 |
+
.verdict { position:relative; top:auto; min-height:0; }
|
| 215 |
+
.empty { min-height:220px; }
|
| 216 |
+
.domains .chip { flex:1 1 calc(50% - 4px); }
|
| 217 |
+
}
|
| 218 |
+
@media (max-width: 420px) {
|
| 219 |
+
.wrap { padding:16px 14px 40px; }
|
| 220 |
+
.case, .verdict { padding:16px; }
|
| 221 |
+
.jurors { grid-template-columns:1fr; gap:6px; }
|
| 222 |
+
.juror { min-height:0; display:grid; grid-template-columns:minmax(0,1fr) auto; column-gap:12px; padding:9px 11px; }
|
| 223 |
+
.juror .who, .juror .role { grid-column:1; }
|
| 224 |
+
.juror .vote { grid-column:2; grid-row:1; margin:0; text-align:right; }
|
| 225 |
+
.juror .agree { grid-column:2; grid-row:2; margin:0; text-align:right; }
|
| 226 |
+
.seal { width:112px; height:112px; }
|
| 227 |
+
.seal .num b { font-size:1rem; }
|
| 228 |
+
}
|
| 229 |
</style>
|
| 230 |
</head>
|
| 231 |
<body>
|
| 232 |
<div class="wrap">
|
| 233 |
+
|
| 234 |
+
<header class="mast">
|
| 235 |
+
<div class="brand">Quorum<small>a jury of tiny open models</small></div>
|
| 236 |
+
<nav aria-label="Project links">
|
| 237 |
+
<a href="https://github.com/DataAgent-Lab/Quorum" target="_blank" rel="noopener">GitHub</a>
|
| 238 |
+
<a href="https://huggingface.co/collections/DataAgent/quorum-6abb3255e0fab20d66add39b" target="_blank" rel="noopener">Models</a>
|
| 239 |
+
<a href="https://huggingface.co/DataAgent/Quorum-Reader-Qwen3-4B-Adapter" target="_blank" rel="noopener">24-shot adapter</a>
|
| 240 |
+
</nav>
|
| 241 |
+
</header>
|
| 242 |
+
|
| 243 |
+
<section class="hero">
|
| 244 |
+
<h1>Ask the <em>jury</em>.</h1>
|
| 245 |
+
<p>Three sub-1B open models read a message and each casts a vote. Quorum returns the verdict and a confidence
|
| 246 |
+
you can act on — no training, no paid API. Every example is a real test sentence from a public benchmark, so
|
| 247 |
+
you can see when the jury gets it right, and when it doesn't.</p>
|
| 248 |
+
</section>
|
| 249 |
+
|
| 250 |
+
<main class="court">
|
| 251 |
+
<section class="case card" aria-labelledby="case-title">
|
| 252 |
+
<h2 id="case-title" class="step"><b>01</b> Choose a domain</h2>
|
| 253 |
+
<div class="domains" id="domains" role="group" aria-label="Domains"></div>
|
| 254 |
+
|
| 255 |
+
<div class="step" style="margin-top:22px"><b>02</b> The message</div>
|
| 256 |
+
<div class="fieldhead">
|
| 257 |
+
<span class="tag" id="msg-tag"></span>
|
| 258 |
+
<button class="linkbtn" id="another" type="button">🎲 Another real sentence</button>
|
| 259 |
+
</div>
|
| 260 |
+
<label for="msg" class="sr">Message</label>
|
| 261 |
+
<textarea id="msg" rows="2" spellcheck="false"></textarea>
|
| 262 |
+
|
| 263 |
+
<div class="step" style="margin-top:22px"><b>03</b> Candidate labels</div>
|
| 264 |
+
<div id="labels-area"></div>
|
| 265 |
+
<div class="hint" id="labels-hint"></div>
|
| 266 |
+
|
| 267 |
+
<div class="actions">
|
| 268 |
+
<button class="btn" id="go" type="button">Hear the verdict<span class="arrow">→</span></button>
|
| 269 |
+
<div class="privacy">Sentences you type yourself are kept on the demo server so repeats answer instantly.</div>
|
| 270 |
+
</div>
|
| 271 |
+
</section>
|
| 272 |
+
|
| 273 |
+
<section class="verdict card" id="verdict" aria-live="polite" aria-label="Verdict">
|
| 274 |
+
<div class="empty" id="empty">
|
| 275 |
+
<div><div class="glyph">⚖</div><p>Pick a domain and hear the verdict. The three jurors vote, then Quorum
|
| 276 |
+
rules — and the dataset tells you whether it was right.</p></div>
|
| 277 |
+
</div>
|
| 278 |
+
</section>
|
| 279 |
+
</main>
|
| 280 |
|
| 281 |
<footer>
|
| 282 |
+
<div>Test sentences: Banking77, CLINC150, HWU64, MASSIVE, MTOP, SNIPS and Bitext — public research datasets.</div>
|
| 283 |
+
<div><a href="https://github.com/DataAgent-Lab/Quorum/issues" target="_blank" rel="noopener">Want this on your own labels? Start a conversation →</a></div>
|
|
|
|
| 284 |
</footer>
|
| 285 |
</div>
|
| 286 |
|
| 287 |
+
<style>.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}</style>
|
| 288 |
<script>
|
| 289 |
+
let DATA = null; // space/samples.json — real benchmark test sentences + description labels
|
| 290 |
+
const DEFAULT_API = "https://quorum-api.idataagent.com";
|
| 291 |
+
// ?api= is honoured only when the page itself runs locally (testing / self-hosting). On the public page a crafted
|
| 292 |
+
// link must never redirect what visitors type to another server.
|
| 293 |
+
const LOCAL_PAGE = ["localhost", "127.0.0.1", "[::1]", ""].includes(location.hostname);
|
| 294 |
+
const API_BASE = ((LOCAL_PAGE && new URLSearchParams(location.search).get("api")) || DEFAULT_API).replace(/\/$/, "");
|
| 295 |
+
const HIT_TIMEOUT = 30000, LIVE_TIMEOUT = 180000; // a cold start (models load on demand) takes ~30-90 s
|
| 296 |
+
const LIVE_LIMIT = 50; // labels the server computes live; larger sets are cache-only
|
| 297 |
+
const MEMBERS = [["PrismNLI-0.4B", "NLI reader"], ["bge-large", "Embedder · large"], ["bge-base", "Embedder · base"]];
|
| 298 |
+
const OWN = { key: "own", title: "Your own labels", dataset: "free text",
|
| 299 |
+
msg: "I was charged twice for the same order and want my money back",
|
| 300 |
+
labels: ["billing / double charge", "refund request", "order status", "cancel subscription", "technical bug", "general question"] };
|
| 301 |
+
|
| 302 |
+
const $ = id => document.getElementById(id);
|
| 303 |
+
const esc = s => String(s).replace(/[&<>"']/g, c => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[c]));
|
| 304 |
+
const state = { ds: null, sample: null, edited: false, showLabels: false };
|
| 305 |
+
|
| 306 |
+
/* ---------- domains ---------- */
|
| 307 |
+
function renderDomains() {
|
| 308 |
+
const all = [...DATA.domains, OWN];
|
| 309 |
+
$("domains").innerHTML = all.map(d => `<button type="button" class="chip${d.key === "own" ? " own" : ""}" data-key="${d.key}"
|
| 310 |
+
aria-pressed="false"><span class="t">${esc(d.title)}</span><span class="s">${d.key === "own" ? "type anything"
|
| 311 |
+
: `${esc(d.dataset)} · ${d.labels.length}`}</span></button>`).join("");
|
| 312 |
+
$("domains").querySelectorAll(".chip").forEach(b => b.onclick = () => select(b.dataset.key));
|
| 313 |
+
}
|
| 314 |
+
function select(key) {
|
| 315 |
+
state.ds = key === "own" ? OWN : DATA.domains.find(d => d.key === key);
|
| 316 |
+
$("domains").querySelectorAll(".chip").forEach(b => b.setAttribute("aria-pressed", String(b.dataset.key === key)));
|
| 317 |
+
state.showLabels = false;
|
| 318 |
+
if (key === "own") { state.sample = null; $("msg").value = OWN.msg; }
|
| 319 |
+
else draw();
|
| 320 |
+
renderLabels(); syncMessageState();
|
| 321 |
+
}
|
| 322 |
+
function draw() {
|
| 323 |
+
const d = state.ds, prev = state.sample;
|
| 324 |
+
let s; do { s = d.samples[Math.floor(Math.random() * d.samples.length)]; } while (d.samples.length > 1 && s === prev);
|
| 325 |
+
state.sample = s; $("msg").value = s.t; state.edited = false; syncMessageState();
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
/* ---------- labels ---------- */
|
| 329 |
+
function currentLabels() { // [{name, desc, send}] — `send` is the exact string sent to the API
|
| 330 |
+
if (state.ds.key === "own") {
|
| 331 |
+
const seen = new Set();
|
| 332 |
+
return ($("own-labels") ? $("own-labels").value : OWN.labels.join("\n")).split("\n").map(s => s.trim())
|
| 333 |
+
.filter(s => s && !seen.has(s) && seen.add(s)).map(s => ({ name: s, desc: null, send: s }));
|
| 334 |
}
|
| 335 |
+
return state.ds.labels.map(l => ({ name: l.name, desc: l.desc, send: l.desc || l.name }));
|
| 336 |
+
}
|
| 337 |
+
function renderLabels() {
|
| 338 |
+
const d = state.ds;
|
| 339 |
+
if (d.key === "own") {
|
| 340 |
+
$("labels-area").innerHTML = `<label for="own-labels" class="sr">Candidate labels</label>
|
| 341 |
+
<textarea id="own-labels" spellcheck="false">${esc(OWN.labels.join("\n"))}</textarea>`;
|
| 342 |
+
$("own-labels").addEventListener("input", syncMessageState);
|
| 343 |
+
return;
|
| 344 |
+
}
|
| 345 |
+
const n = d.labels.length;
|
| 346 |
+
$("labels-area").innerHTML = `<div class="labels-sum"><div><span class="n">${n}</span>
|
| 347 |
+
<span class="what">intents · from ${esc(d.dataset)}</span></div>
|
| 348 |
+
<button class="linkbtn" id="toggle-labels" type="button" aria-expanded="${state.showLabels}">${state.showLabels ? "Hide" : "Show"} labels</button></div>
|
| 349 |
+
${state.showLabels ? `<ul class="labels-list">${d.labels.map(l => `<li><span class="nm">${esc(l.name)}</span>${l.desc
|
| 350 |
+
? `<span class="ds">${esc(l.desc)}</span>` : ""}</li>`).join("")}</ul>` : ""}`;
|
| 351 |
+
$("toggle-labels").onclick = () => { state.showLabels = !state.showLabels; renderLabels(); };
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
/* ---------- message state ---------- */
|
| 355 |
+
function syncMessageState() {
|
| 356 |
+
const d = state.ds, own = d.key === "own";
|
| 357 |
+
state.edited = !own && state.sample && $("msg").value.trim() !== state.sample.t;
|
| 358 |
+
const tag = $("msg-tag");
|
| 359 |
+
if (own) { tag.className = "tag edited"; tag.textContent = "Your sentence"; }
|
| 360 |
+
else if (state.edited) { tag.className = "tag edited"; tag.textContent = "Edited — no longer a test sentence"; }
|
| 361 |
+
else { tag.className = "tag"; tag.textContent = "Real test sentence"; }
|
| 362 |
+
$("another").hidden = own;
|
| 363 |
+
const n = currentLabels().length, hint = $("labels-hint"), go = $("go");
|
| 364 |
+
hint.className = "hint"; hint.textContent = ""; go.disabled = false;
|
| 365 |
+
if (own && n > LIVE_LIMIT) { hint.className = "hint warn"; hint.textContent = `Up to ${LIVE_LIMIT} labels can be computed live — you have ${n}.`; go.disabled = true; }
|
| 366 |
+
else if (own && n < 2) { hint.className = "hint warn"; hint.textContent = "Add at least 2 labels, one per line."; go.disabled = true; }
|
| 367 |
+
else if (state.edited && n > LIVE_LIMIT) {
|
| 368 |
+
hint.className = "hint warn";
|
| 369 |
+
hint.textContent = `Edited sentences are computed live, which this small server does for up to ${LIVE_LIMIT} labels — ${d.dataset} has ${n}. Use 🎲 for another real sentence, or switch to “Your own labels”.`;
|
| 370 |
+
go.disabled = true;
|
| 371 |
+
} else if (!own) hint.textContent = "The jury ranks every one of these labels for the message.";
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/* ---------- deciding ---------- */
|
| 375 |
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
| 376 |
+
function isOwnDefault() {
|
| 377 |
+
return state.ds.key === "own" && $("msg").value.trim() === OWN.msg &&
|
| 378 |
+
currentLabels().map(l => l.send).join("\n") === OWN.labels.join("\n");
|
| 379 |
+
}
|
| 380 |
+
async function decide() {
|
| 381 |
+
const labels = currentLabels(), message = $("msg").value.trim();
|
| 382 |
+
if (!message || $("go").disabled) return;
|
| 383 |
+
const gold = (!state.edited && state.sample) ? state.sample.g : null;
|
| 384 |
+
const maybeLive = state.ds.key === "own" ? !isOwnDefault() : state.edited; // everything else is pre-computed
|
| 385 |
+
$("go").disabled = true;
|
| 386 |
+
showDeliberating();
|
| 387 |
+
const timed = async () => { // the footnote reports the real response time, not the deliberation animation
|
| 388 |
+
const t0 = performance.now(), out = await apiPredict(message, labels, maybeLive);
|
| 389 |
+
return { ...out, secs: (performance.now() - t0) / 1000 };
|
| 390 |
+
};
|
| 391 |
+
try {
|
| 392 |
+
const [{ res, source, secs }] = await Promise.all([timed(), sleep(350)]);
|
| 393 |
+
showVerdict(res, labels, gold, source, secs);
|
| 394 |
+
} catch (e) { showError(e); }
|
| 395 |
+
finally { stopClock(); syncMessageState(); }
|
| 396 |
+
}
|
| 397 |
+
async function post(message, labels, timeout) {
|
| 398 |
+
const ctrl = new AbortController(), timer = setTimeout(() => ctrl.abort(), timeout);
|
| 399 |
+
try {
|
| 400 |
+
const r = await fetch(API_BASE + "/predict", { method: "POST", headers: { "content-type": "application/json" },
|
| 401 |
+
body: JSON.stringify({ message, labels: labels.map(l => l.send) }), signal: ctrl.signal });
|
| 402 |
+
if (!r.ok) {
|
| 403 |
+
let detail = ""; try { detail = (await r.json()).detail || ""; } catch (_) {}
|
| 404 |
+
throw Object.assign(new Error(typeof detail === "string" ? detail : ""), { status: r.status });
|
| 405 |
}
|
| 406 |
+
return { res: await r.json(), source: r.headers.get("X-Quorum-Cache") || "miss" };
|
| 407 |
+
} finally { clearTimeout(timer); }
|
| 408 |
+
}
|
| 409 |
+
async function modelAwake() {
|
| 410 |
+
try { const r = await fetch(API_BASE + "/health", { signal: AbortSignal.timeout(5000) });
|
| 411 |
+
return (await r.json()).model_loaded; } catch (_) { return null; }
|
| 412 |
+
}
|
| 413 |
+
async function apiPredict(message, labels, maybeLive) {
|
| 414 |
+
const timeout = maybeLive ? LIVE_TIMEOUT : HIT_TIMEOUT;
|
| 415 |
+
if (maybeLive && (await modelAwake()) === false) showWaking();
|
| 416 |
+
try { return await post(message, labels, timeout); }
|
| 417 |
+
catch (e) {
|
| 418 |
+
// Timeouts, network errors, 503 (busy / restarting) and 504 are worth one retry: the server keeps a result it
|
| 419 |
+
// finishes, so the second attempt is usually answered from the cache.
|
| 420 |
+
const retriable = e.name === "AbortError" || e.name === "TypeError" || e.status === 503 || e.status === 504;
|
| 421 |
+
if (!retriable) throw e;
|
| 422 |
+
showWaking("Still working — trying once more…");
|
| 423 |
+
await sleep(3000);
|
| 424 |
+
return await post(message, labels, timeout);
|
| 425 |
}
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
/* ---------- rendering ---------- */
|
| 429 |
+
function jurorsHTML(members, verdictSend, byS) {
|
| 430 |
+
return `<div class="jurors">${MEMBERS.map(([m, role], i) => {
|
| 431 |
+
const mv = members && members[i];
|
| 432 |
+
if (!mv) return `<div class="juror thinking"><div class="who">${m}</div><div class="role">${role}</div>
|
| 433 |
+
<div class="vote"><span class="dots"><span></span><span></span><span></span></span></div></div>`;
|
| 434 |
+
const lab = byS.get(mv.pick), agree = mv.pick === verdictSend;
|
| 435 |
+
return `<div class="juror"><div class="who">${m}</div><div class="role">${role}</div>
|
| 436 |
+
<div class="vote">${esc(lab ? lab.name : mv.pick)}</div>
|
| 437 |
+
<div class="agree ${agree ? "y" : "n"}">${agree ? "✓ with the verdict" : "≠ dissents"}</div></div>`;
|
| 438 |
+
}).join("")}</div>`;
|
| 439 |
+
}
|
| 440 |
+
let clock = null;
|
| 441 |
+
function showDeliberating() {
|
| 442 |
+
const v = $("verdict"); v.className = "verdict card deliberating";
|
| 443 |
+
v.innerHTML = jurorsHTML(null) + `<div id="delib-note"><p class="hint" style="margin-top:16px">The jury is deliberating…</p></div>`;
|
| 444 |
+
}
|
| 445 |
+
function showWaking(headline) {
|
| 446 |
+
const note = $("delib-note"); if (!note) return;
|
| 447 |
+
const t0 = clock ? clock.t0 : performance.now();
|
| 448 |
+
note.innerHTML = `<div class="waking"><b>${esc(headline || "Waking the jury up")}</b>
|
| 449 |
+
<p>The models sleep when the demo is quiet and load on demand, so this first answer can take a minute or two.
|
| 450 |
+
Answers after that are quick, and anything already computed comes back instantly.</p>
|
| 451 |
+
<div class="clock" id="clock">0 s</div></div>`;
|
| 452 |
+
stopClock();
|
| 453 |
+
clock = { t0, id: setInterval(() => { const c = $("clock"); if (c) c.textContent = `${Math.round((performance.now() - t0) / 1000)} s`; }, 1000) };
|
| 454 |
+
}
|
| 455 |
+
function stopClock() { if (clock) { clearInterval(clock.id); clock = null; } }
|
| 456 |
+
function showVerdict(res, labels, gold, source, secs) {
|
| 457 |
+
const byS = new Map(labels.map((l, i) => [l.send, { ...l, i }]));
|
| 458 |
+
const top = byS.get(res.label), pct = res.confidence * 100;
|
| 459 |
+
const entries = Object.entries(res.distribution);
|
| 460 |
+
const truth = gold == null ? `<div class="truth na"><span class="ic">i</span><div>Not a test sentence, so there is no
|
| 461 |
+
reference answer to check against.</div></div>`
|
| 462 |
+
: gold === top.i ? `<div class="truth ok"><span class="ic">✓</span><div>Correct — this is the dataset's answer.
|
| 463 |
+
<small>${accLine()}</small></div></div>`
|
| 464 |
+
: `<div class="truth no"><span class="ic">✗</span><div>The dataset's answer is <b>${esc(labels[gold].name)}</b>.
|
| 465 |
+
<small>${accLine()}</small></div></div>`;
|
| 466 |
+
const bar = ([s, p], i) => { const l = byS.get(s);
|
| 467 |
+
return `<div class="bar-row${i === 0 ? " top" : ""}"><div class="lab" title="${esc(l.desc || l.name)}">${esc(l.name)}${l.desc
|
| 468 |
+
? `<span>${esc(l.desc)}</span>` : ""}</div><div class="pct">${(p * 100).toFixed(1)}%</div>
|
| 469 |
+
<div class="track"><div class="fill" data-w="${Math.max(1.5, p * 100)}"></div></div></div>`; };
|
| 470 |
+
const v = $("verdict"); v.className = "verdict card reveal";
|
| 471 |
+
v.innerHTML = jurorsHTML(res.members, res.label, byS) + `
|
| 472 |
+
<div class="ruling"><div><div class="eyebrow">The verdict</div><div class="name">${esc(top.name)}</div>
|
| 473 |
+
${top.desc ? `<div class="desc">${esc(top.desc)}</div>` : ""}</div>
|
| 474 |
+
<div class="seal" aria-label="confidence ${pct.toFixed(1)} percent">${sealSVG()}<div class="num"><div>
|
| 475 |
+
<b id="conf">0.0%</b><i>confidence</i></div></div></div></div>
|
| 476 |
+
${truth}
|
| 477 |
+
<div class="bars">${entries.slice(0, 5).map(bar).join("")}<div id="more-bars" hidden>${entries.slice(5).map((e, i) => bar(e, i + 5)).join("")}</div>
|
| 478 |
+
${entries.length > 5 ? `<button class="linkbtn more" id="more" type="button" aria-expanded="false">Show all ${entries.length} labels</button>` : ""}</div>
|
| 479 |
+
<div class="gate"><div class="gate-head"><span>Auto-act when confidence ≥ <b id="tauv">70</b>%</span><span>else hand to a human</span></div>
|
| 480 |
+
<input type="range" id="tau" min="0" max="100" value="70" aria-label="Confidence threshold" />
|
| 481 |
+
<div class="decision" id="decision"></div></div>
|
| 482 |
+
<div class="footnote"><span>${source === "hit" ? "answered from the cache" : "computed live"} in ${secs.toFixed(2)} s</span>
|
| 483 |
+
<span>${state.ds.key === "own" ? "zero-shot · your labels" : `zero-shot · ${esc(state.ds.dataset)} · label descriptions`}</span></div>`;
|
| 484 |
+
requestAnimationFrame(() => setTimeout(() => v.querySelectorAll(".bars > .bar-row .fill").forEach((f, i) =>
|
| 485 |
+
setTimeout(() => f.style.width = f.dataset.w + "%", matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 950 + i * 70)), 0));
|
| 486 |
+
countUp($("conf"), pct);
|
| 487 |
+
if ($("more")) $("more").onclick = () => { const m = $("more-bars"), open = m.hidden; m.hidden = !open;
|
| 488 |
+
$("more").setAttribute("aria-expanded", String(open)); $("more").textContent = open ? "Show top 5" : `Show all ${entries.length} labels`;
|
| 489 |
+
m.querySelectorAll(".fill").forEach(f => f.style.width = open ? f.dataset.w + "%" : 0); };
|
| 490 |
+
const gate = () => { const t = +$("tau").value; $("tauv").textContent = t; const d = $("decision");
|
| 491 |
+
d.className = "decision " + (pct >= t ? "act" : "esc");
|
| 492 |
+
d.textContent = pct >= t ? `Auto-act — ${pct.toFixed(1)}% ≥ ${t}%` : `Escalate to a human — ${pct.toFixed(1)}% < ${t}%`; };
|
| 493 |
+
$("tau").oninput = gate; gate();
|
| 494 |
+
}
|
| 495 |
+
function accLine() { const d = state.ds;
|
| 496 |
+
return `Quorum gets ${(d.accuracy * 100).toFixed(1)}% of ${d.n_test.toLocaleString()} ${esc(d.dataset)} test sentences right (zero-shot).`; }
|
| 497 |
+
function sealSVG() {
|
| 498 |
+
const id = "c" + Math.random().toString(36).slice(2, 7);
|
| 499 |
+
return `<svg viewBox="0 0 120 120" aria-hidden="true"><defs><path id="${id}" d="M60,60 m-48,0 a48,48 0 1,1 96,0 a48,48 0 1,1 -96,0"/></defs>
|
| 500 |
+
<circle cx="60" cy="60" r="58" fill="none" stroke="var(--seal)" stroke-width="1.4"/>
|
| 501 |
+
<circle cx="60" cy="60" r="40" fill="var(--seal-soft)" stroke="var(--seal)" stroke-width="1"/>
|
| 502 |
+
<text font-family="IBM Plex Mono, monospace" font-size="7.4" letter-spacing="2.9" fill="var(--seal)">
|
| 503 |
+
<textPath href="#${id}">QUORUM · VERDICT · QUORUM · VERDICT ·</textPath></text></svg>`;
|
| 504 |
+
}
|
| 505 |
+
function countUp(el, target) {
|
| 506 |
+
if (matchMedia("(prefers-reduced-motion: reduce)").matches) { el.textContent = target.toFixed(1) + "%"; return; }
|
| 507 |
+
const t0 = performance.now() + 620, dur = 800;
|
| 508 |
+
const tick = now => { const k = Math.min(1, Math.max(0, (now - t0) / dur)), e = 1 - Math.pow(1 - k, 3);
|
| 509 |
+
el.textContent = (target * e).toFixed(1) + "%"; if (k < 1) requestAnimationFrame(tick); };
|
| 510 |
+
requestAnimationFrame(tick);
|
| 511 |
+
}
|
| 512 |
+
function showError(e) {
|
| 513 |
+
const v = $("verdict"); v.className = "verdict card";
|
| 514 |
+
const msg = e && e.status === 422 && e.message ? e.message
|
| 515 |
+
: "The jury couldn't answer just now. The demo runs on a small shared server, so please try again in a moment.";
|
| 516 |
+
v.innerHTML = `<div class="err">${esc(msg)}</div>${e && e.status === 422 ? "" :
|
| 517 |
+
`<button class="btn" id="retry" type="button">Try again<span class="arrow">→</span></button>`}`;
|
| 518 |
+
if ($("retry")) $("retry").onclick = decide;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/* ---------- boot ---------- */
|
| 522 |
+
$("msg").addEventListener("input", syncMessageState);
|
| 523 |
+
$("another").onclick = draw;
|
| 524 |
+
$("go").onclick = decide;
|
| 525 |
+
$("go").disabled = true;
|
| 526 |
+
fetch("samples.json").then(r => { if (!r.ok) throw new Error(r.status); return r.json(); }).then(d => {
|
| 527 |
+
DATA = d; renderDomains(); select("banking77");
|
| 528 |
+
}).catch(() => {
|
| 529 |
+
$("verdict").innerHTML = `<div class="err">The examples could not be loaded — please refresh the page.</div>`;
|
| 530 |
+
});
|
| 531 |
</script>
|
| 532 |
</body>
|
| 533 |
</html>
|
samples.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|