Spaces:
Running
Running
Download index.html from DataAgent/Quorum-Demo: direct link, hf CLI and curl.
- Browser
- Download file 34.5 kB
-
https://huggingface.co/spaces/DataAgent/Quorum-Demo/resolve/main/index.html
- Command line
-
hf download hf://spaces/DataAgent/Quorum-Demo/index.html
-
curl -L -o index.html https://huggingface.co/spaces/DataAgent/Quorum-Demo/resolve/main/index.html
34.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Quorum — ask the jury</title> | |
| <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." /> | |
| <meta property="og:type" content="website" /> | |
| <meta property="og:title" content="Quorum — ask the jury" /> | |
| <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." /> | |
| <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='%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" /> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <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" /> | |
| <style> | |
| :root { | |
| --paper:#f4efe5; --paper-2:#ece4d5; --card:#fbf8f1; --ink:#1c1813; --ink-2:#4b443a; --muted:#7c7367; | |
| --line:#ddd3c3; --line-2:#cbbfab; --seal:#b8391f; --seal-2:#d9573a; --seal-soft:#f5ddd3; | |
| --ok:#2c6a3c; --ok-soft:#e1efe1; --no:#a0341f; --no-soft:#f6e1da; | |
| --shadow: 0 1px 0 rgba(28,24,19,.04), 0 10px 30px -18px rgba(28,24,19,.35); | |
| --serif: "Instrument Serif", "Iowan Old Style", Georgia, serif; | |
| --sans: "Hanken Grotesk", ui-sans-serif, system-ui, sans-serif; | |
| --mono: "IBM Plex Mono", ui-monospace, Menlo, monospace; | |
| --ease: cubic-bezier(.2,.8,.2,1); | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root:not([data-theme="light"]) { | |
| --paper:#13100c; --paper-2:#1a1611; --card:#1c1813; --ink:#f0e8da; --ink-2:#cfc4b3; --muted:#9b9081; | |
| --line:#2e2821; --line-2:#3c342b; --seal:#ee6a49; --seal-2:#ff8a6a; --seal-soft:#3a1d15; | |
| --ok:#86c897; --ok-soft:#16271b; --no:#f2907a; --no-soft:#35190f; | |
| --shadow: 0 1px 0 rgba(0,0,0,.4), 0 16px 40px -20px rgba(0,0,0,.8); | |
| } | |
| } | |
| * { box-sizing: border-box; } | |
| html { -webkit-text-size-adjust: 100%; } | |
| body { margin:0; font-family:var(--sans); color:var(--ink); background:var(--paper); line-height:1.55; | |
| font-size:15.5px; min-height:100vh; } | |
| body::before { /* paper grain */ | |
| content:""; position:fixed; inset:0; pointer-events:none; z-index:0; opacity:.5; | |
| 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"); | |
| } | |
| .wrap { position:relative; z-index:1; max-width:1120px; margin:0 auto; padding:22px 20px 56px; } | |
| a { color:inherit; } | |
| /* masthead */ | |
| .mast { display:flex; justify-content:space-between; align-items:baseline; gap:16px; flex-wrap:wrap; | |
| border-bottom:1px solid var(--line); padding-bottom:14px; } | |
| .brand { font:400 1.65rem/1 var(--serif); letter-spacing:.01em; } | |
| .brand small { font:500 .78rem var(--mono); color:var(--muted); margin-left:10px; letter-spacing:.04em; } | |
| .mast nav { display:flex; gap:18px; font-size:.88rem; } | |
| .mast nav a { text-decoration:none; color:var(--ink-2); border-bottom:1px solid transparent; } | |
| .mast nav a:hover { border-color:var(--ink-2); } | |
| /* hero */ | |
| .hero { display:grid; grid-template-columns: 1.15fr .85fr; gap:28px; align-items:end; margin:34px 0 26px; } | |
| .hero h1 { font:400 clamp(2.6rem, 6vw, 4.6rem)/.95 var(--serif); margin:0; letter-spacing:-.01em; } | |
| .hero h1 em { color:var(--seal); } | |
| .hero p { margin:0; color:var(--ink-2); font-size:1rem; max-width:44ch; } | |
| /* court: case | verdict */ | |
| .court { display:grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap:22px; align-items:start; } | |
| .card { background:var(--card); border:1px solid var(--line); border-radius:16px; box-shadow:var(--shadow); } | |
| .case { padding:22px 22px 20px; } | |
| .step { display:flex; align-items:baseline; gap:10px; font:500 .72rem/1 var(--mono); letter-spacing:.12em; | |
| text-transform:uppercase; color:var(--muted); margin:2px 0 12px; } | |
| .step b { color:var(--seal); font-weight:500; } | |
| .step + * + .step, .block + .step { margin-top:22px; } | |
| /* domain chips */ | |
| .domains { display:flex; flex-wrap:wrap; gap:8px; } | |
| .chip { text-align:left; border:1px solid var(--line); background:var(--paper); color:var(--ink); | |
| border-radius:12px; padding:9px 11px 8px; cursor:pointer; font:inherit; | |
| transition: border-color .2s, background .2s, transform .2s var(--ease); } | |
| .chip:hover { border-color:var(--line-2); transform:translateY(-1px); } | |
| .chip .t { display:block; font-weight:600; font-size:.88rem; line-height:1.2; white-space:nowrap; } | |
| .chip .s { display:block; font:400 .7rem var(--mono); color:var(--muted); margin-top:3px; white-space:nowrap; } | |
| .chip[aria-pressed="true"] { background:var(--ink); color:var(--paper); border-color:var(--ink); } | |
| .chip[aria-pressed="true"] .s { color:color-mix(in srgb, var(--paper) 70%, transparent); } | |
| .chip.own .t::before { content:"✎ "; } | |
| .chip:focus-visible, .btn:focus-visible, .linkbtn:focus-visible, textarea:focus-visible, input:focus-visible { | |
| outline:2px solid var(--seal); outline-offset:2px; } | |
| /* message */ | |
| .fieldhead { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:7px; } | |
| .tag { font:500 .7rem/1 var(--mono); letter-spacing:.04em; color:var(--ok); background:var(--ok-soft); | |
| border-radius:999px; padding:5px 9px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } | |
| .tag.edited { color:var(--muted); background:var(--paper-2); } | |
| .linkbtn { background:none; border:0; padding:4px 2px; color:var(--ink-2); font:500 .82rem var(--sans); | |
| cursor:pointer; text-decoration:underline; text-decoration-color:var(--line-2); text-underline-offset:3px; } | |
| .linkbtn:hover { color:var(--seal); text-decoration-color:var(--seal); } | |
| textarea { width:100%; border:1px solid var(--line); border-radius:12px; background:var(--paper); color:var(--ink); | |
| font: 400 1.12rem/1.45 var(--serif); padding:12px 14px; resize:vertical; min-height:74px; } | |
| textarea#own-labels { font:400 .92rem/1.5 var(--sans); min-height:140px; } | |
| /* labels summary */ | |
| .labels-sum { display:flex; justify-content:space-between; align-items:center; gap:12px; border:1px solid var(--line); | |
| border-radius:12px; background:var(--paper); padding:11px 14px; } | |
| .labels-sum .n { font:400 1.35rem/1 var(--serif); } | |
| .labels-sum .what { font-size:.88rem; color:var(--ink-2); } | |
| .labels-list { margin-top:8px; max-height:220px; overflow:auto; border:1px solid var(--line); border-radius:12px; | |
| padding:4px 0; background:var(--paper); } | |
| .labels-list li { list-style:none; padding:6px 14px; border-bottom:1px dashed var(--line); } | |
| .labels-list li:last-child { border-bottom:0; } | |
| .labels-list .nm { font-weight:600; font-size:.88rem; } | |
| .labels-list .ds { display:block; font-size:.76rem; color:var(--muted); } | |
| .hint { font-size:.8rem; color:var(--muted); margin-top:6px; } | |
| .hint.warn { color:var(--no); } | |
| .actions { display:flex; align-items:center; gap:14px; margin-top:22px; flex-wrap:wrap; } | |
| .btn { position:relative; border:0; border-radius:12px; padding:13px 22px 12px; cursor:pointer; | |
| background:var(--ink); color:var(--paper); font:600 .98rem var(--sans); letter-spacing:.01em; | |
| transition: transform .2s var(--ease), background .2s; } | |
| .btn:hover { transform:translateY(-1px); background:var(--seal); } | |
| .btn:disabled { opacity:.45; cursor:not-allowed; transform:none; background:var(--ink); } | |
| .btn .arrow { display:inline-block; transition:transform .25s var(--ease); margin-left:6px; } | |
| .btn:hover .arrow { transform:translateX(3px); } | |
| .privacy { font-size:.74rem; color:var(--muted); max-width:34ch; } | |
| /* verdict panel */ | |
| .verdict { padding:22px; min-height:520px; position:sticky; top:18px; overflow:hidden; } | |
| .empty { height:100%; min-height:470px; display:grid; place-items:center; text-align:center; color:var(--muted); } | |
| .empty .glyph { font:400 4.2rem/1 var(--serif); color:var(--line-2); } | |
| .empty p { max-width:30ch; margin:10px auto 0; font-size:.92rem; } | |
| .jurors { display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:8px; perspective:900px; } | |
| .juror { border:1px solid var(--line); border-radius:12px; padding:10px 10px 9px; background:var(--paper); | |
| min-height:86px; transform-origin:50% 0; } | |
| .juror .who { font:500 .66rem/1.2 var(--mono); letter-spacing:.05em; color:var(--muted); text-transform:uppercase; } | |
| .juror .role { font-size:.72rem; color:var(--muted); } | |
| .juror .vote { margin-top:7px; font-weight:600; font-size:.86rem; line-height:1.25; } | |
| .juror .agree { font:500 .66rem var(--mono); margin-top:4px; letter-spacing:.04em; } | |
| .juror .agree.y { color:var(--ok); } .juror .agree.n { color:var(--no); } | |
| .juror.thinking .vote { color:var(--muted); } | |
| .dots span { display:inline-block; width:5px; height:5px; border-radius:50%; background:var(--muted); margin-right:3px; | |
| animation: blink 1.2s infinite both; } | |
| .dots span:nth-child(2) { animation-delay:.2s } .dots span:nth-child(3) { animation-delay:.4s } | |
| @keyframes blink { 0%,80%,100% { opacity:.2 } 40% { opacity:1 } } | |
| .ruling { display:grid; grid-template-columns: minmax(0,1fr) auto; gap:16px; align-items:center; | |
| margin:22px 0 6px; padding:18px 0 16px; border-top:1px solid var(--line); border-bottom:1px solid var(--line); } | |
| .eyebrow { font:500 .7rem/1 var(--mono); letter-spacing:.14em; text-transform:uppercase; color:var(--seal); } | |
| .ruling .name { font:400 clamp(2rem, 4.2vw, 2.9rem)/1 var(--serif); margin:8px 0 6px; overflow-wrap:anywhere; } | |
| .ruling .desc { font-size:.86rem; color:var(--ink-2); } | |
| .seal { width:136px; height:136px; position:relative; flex:none; } | |
| .seal svg { position:absolute; inset:0; } | |
| .seal .num { position:absolute; inset:0; display:grid; place-items:center; text-align:center; color:var(--seal); } | |
| .seal .num b { display:block; font:500 1.18rem/1 var(--mono); letter-spacing:-.03em; } | |
| .seal .num i { display:block; font:500 .5rem/1 var(--mono); letter-spacing:.18em; text-transform:uppercase; | |
| font-style:normal; margin-top:5px; opacity:.85; } | |
| .truth { display:flex; gap:10px; align-items:flex-start; border-radius:12px; padding:11px 13px; margin-top:14px; | |
| font-size:.9rem; } | |
| .truth .ic { font:600 1rem/1.3 var(--mono); } | |
| .truth.ok { background:var(--ok-soft); color:var(--ok); } | |
| .truth.no { background:var(--no-soft); color:var(--no); } | |
| .truth.na { background:var(--paper-2); color:var(--ink-2); } | |
| .truth small { display:block; color:var(--ink-2); opacity:.85; margin-top:2px; font-size:.78rem; } | |
| .bars { margin-top:16px; } | |
| .bar-row { display:grid; grid-template-columns: minmax(0, 1fr) 54px; gap:4px 10px; align-items:center; padding:6px 0; } | |
| .bar-row .lab { font-size:.86rem; font-weight:600; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } | |
| .bar-row .lab span { font-weight:400; color:var(--muted); font-size:.74rem; margin-left:6px; } | |
| .bar-row .pct { font:500 .8rem var(--mono); text-align:right; color:var(--ink-2); grid-row: span 2; } | |
| .track { height:6px; background:var(--paper-2); border-radius:99px; overflow:hidden; } | |
| .fill { height:100%; width:0; background:var(--line-2); border-radius:99px; transition: width .9s var(--ease); } | |
| .bar-row.top .fill { background:var(--seal); } | |
| .bar-row.top .lab { color:var(--seal); } | |
| .more { margin-top:4px; } | |
| .gate { margin-top:18px; padding-top:14px; border-top:1px dashed var(--line); } | |
| .gate-head { display:flex; justify-content:space-between; font-size:.84rem; color:var(--ink-2); } | |
| .gate-head b { font-family:var(--mono); font-weight:500; } | |
| input[type=range] { width:100%; accent-color:var(--seal); margin:10px 0 6px; } | |
| .decision { border-radius:10px; padding:10px 12px; font-weight:600; font-size:.9rem; } | |
| .decision.act { background:var(--ok-soft); color:var(--ok); } | |
| .decision.esc { background:var(--no-soft); color:var(--no); } | |
| .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; } | |
| .err { margin-top:14px; color:var(--no); font-size:.95rem; } | |
| .err .btn { margin-top:14px; } | |
| .waking { margin-top:18px; border:1px dashed var(--line-2); border-radius:12px; padding:14px 16px; } | |
| .waking b { display:block; font:400 1.35rem/1.15 var(--serif); } | |
| .waking p { margin:6px 0 0; font-size:.86rem; color:var(--ink-2); } | |
| .waking .clock { font:500 .8rem var(--mono); color:var(--seal); margin-top:8px; } | |
| /* reveal choreography */ | |
| @media (prefers-reduced-motion: no-preference) { | |
| .reveal .juror { animation: flip .55s var(--ease) both; } | |
| .reveal .juror:nth-child(2) { animation-delay:.12s } .reveal .juror:nth-child(3) { animation-delay:.24s } | |
| .reveal .ruling .eyebrow, .reveal .ruling .name, .reveal .ruling .desc { animation: rise .6s var(--ease) both; } | |
| .reveal .ruling .eyebrow { animation-delay:.42s } .reveal .ruling .name { animation-delay:.5s } | |
| .reveal .ruling .desc { animation-delay:.6s } | |
| .reveal .seal { animation: stamp .55s cubic-bezier(.3,1.6,.5,1) .62s both; } | |
| .reveal .truth { animation: rise .5s var(--ease) 1.05s both; } | |
| .reveal .bars, .reveal .gate, .reveal .footnote { animation: fade .6s var(--ease) .9s both; } | |
| .verdict.deliberating .juror { animation: pulse 1.4s ease-in-out infinite; } | |
| } | |
| @keyframes flip { from { transform: rotateX(-88deg); opacity:0 } to { transform:none; opacity:1 } } | |
| @keyframes rise { from { transform: translateY(14px); opacity:0 } to { transform:none; opacity:1 } } | |
| @keyframes fade { from { opacity:0 } to { opacity:1 } } | |
| @keyframes stamp { 0% { transform: scale(1.9) rotate(-18deg); opacity:0 } 60% { opacity:1 } | |
| 100% { transform: scale(1) rotate(-8deg); opacity:1 } } | |
| @keyframes pulse { 0%,100% { opacity:1 } 50% { opacity:.55 } } | |
| .seal { transform: rotate(-8deg); } | |
| footer { margin-top:44px; padding-top:18px; border-top:1px solid var(--line); font-size:.82rem; color:var(--muted); | |
| display:flex; justify-content:space-between; gap:16px; flex-wrap:wrap; } | |
| footer a { color:var(--ink-2); } | |
| @media (max-width: 900px) { | |
| .hero { grid-template-columns:1fr; gap:14px; margin-top:26px; } | |
| .court { grid-template-columns:1fr; } | |
| .verdict { position:relative; top:auto; min-height:0; } | |
| .empty { min-height:220px; } | |
| .domains .chip { flex:1 1 calc(50% - 4px); } | |
| } | |
| @media (max-width: 420px) { | |
| .wrap { padding:16px 14px 40px; } | |
| .case, .verdict { padding:16px; } | |
| .jurors { grid-template-columns:1fr; gap:6px; } | |
| .juror { min-height:0; display:grid; grid-template-columns:minmax(0,1fr) auto; column-gap:12px; padding:9px 11px; } | |
| .juror .who, .juror .role { grid-column:1; } | |
| .juror .vote { grid-column:2; grid-row:1; margin:0; text-align:right; } | |
| .juror .agree { grid-column:2; grid-row:2; margin:0; text-align:right; } | |
| .seal { width:112px; height:112px; } | |
| .seal .num b { font-size:1rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <header class="mast"> | |
| <div class="brand">Quorum<small>a jury of tiny open models</small></div> | |
| <nav aria-label="Project 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> | |
| </nav> | |
| </header> | |
| <section class="hero"> | |
| <h1>Ask the <em>jury</em>.</h1> | |
| <p>Three sub-1B open models read a message and each casts a vote. Quorum returns the verdict and a confidence | |
| you can act on — no training, no paid API. Every example is a real test sentence from a public benchmark, so | |
| you can see when the jury gets it right, and when it doesn't.</p> | |
| </section> | |
| <main class="court"> | |
| <section class="case card" aria-labelledby="case-title"> | |
| <h2 id="case-title" class="step"><b>01</b> Choose a domain</h2> | |
| <div class="domains" id="domains" role="group" aria-label="Domains"></div> | |
| <div class="step" style="margin-top:22px"><b>02</b> The message</div> | |
| <div class="fieldhead"> | |
| <span class="tag" id="msg-tag"></span> | |
| <button class="linkbtn" id="another" type="button">🎲 Another real sentence</button> | |
| </div> | |
| <label for="msg" class="sr">Message</label> | |
| <textarea id="msg" rows="2" spellcheck="false"></textarea> | |
| <div class="step" style="margin-top:22px"><b>03</b> Candidate labels</div> | |
| <div id="labels-area"></div> | |
| <div class="hint" id="labels-hint"></div> | |
| <div class="actions"> | |
| <button class="btn" id="go" type="button">Hear the verdict<span class="arrow">→</span></button> | |
| <div class="privacy">Sentences you type yourself are kept on the demo server so repeats answer instantly.</div> | |
| </div> | |
| </section> | |
| <section class="verdict card" id="verdict" aria-live="polite" aria-label="Verdict"> | |
| <div class="empty" id="empty"> | |
| <div><div class="glyph">⚖</div><p>Pick a domain and hear the verdict. The three jurors vote, then Quorum | |
| rules — and the dataset tells you whether it was right.</p></div> | |
| </div> | |
| </section> | |
| </main> | |
| <footer> | |
| <div>Test sentences: Banking77, CLINC150, HWU64, MASSIVE, MTOP, SNIPS and Bitext — public research datasets.</div> | |
| <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> | |
| </footer> | |
| </div> | |
| <style>.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}</style> | |
| <script> | |
| let DATA = null; // space/samples.json — real benchmark test sentences + description labels | |
| const DEFAULT_API = "https://quorum-api.idataagent.com"; | |
| // ?api= is honoured only when the page itself runs locally (testing / self-hosting). On the public page a crafted | |
| // link must never redirect what visitors type to another server. | |
| const LOCAL_PAGE = ["localhost", "127.0.0.1", "[::1]", ""].includes(location.hostname); | |
| const API_BASE = ((LOCAL_PAGE && new URLSearchParams(location.search).get("api")) || DEFAULT_API).replace(/\/$/, ""); | |
| const HIT_TIMEOUT = 30000, LIVE_TIMEOUT = 180000; // a cold start (models load on demand) takes ~30-90 s | |
| const LIVE_LIMIT = 50; // labels the server computes live; larger sets are cache-only | |
| const MEMBERS = [["PrismNLI-0.4B", "NLI reader"], ["bge-large", "Embedder · large"], ["bge-base", "Embedder · base"]]; | |
| const OWN = { key: "own", title: "Your own labels", dataset: "free text", | |
| 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"] }; | |
| const $ = id => document.getElementById(id); | |
| const esc = s => String(s).replace(/[&<>"']/g, c => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[c])); | |
| const state = { ds: null, sample: null, edited: false, showLabels: false }; | |
| /* ---------- domains ---------- */ | |
| function renderDomains() { | |
| const all = [...DATA.domains, OWN]; | |
| $("domains").innerHTML = all.map(d => `<button type="button" class="chip${d.key === "own" ? " own" : ""}" data-key="${d.key}" | |
| aria-pressed="false"><span class="t">${esc(d.title)}</span><span class="s">${d.key === "own" ? "type anything" | |
| : `${esc(d.dataset)} · ${d.labels.length}`}</span></button>`).join(""); | |
| $("domains").querySelectorAll(".chip").forEach(b => b.onclick = () => select(b.dataset.key)); | |
| } | |
| function select(key) { | |
| state.ds = key === "own" ? OWN : DATA.domains.find(d => d.key === key); | |
| $("domains").querySelectorAll(".chip").forEach(b => b.setAttribute("aria-pressed", String(b.dataset.key === key))); | |
| state.showLabels = false; | |
| if (key === "own") { state.sample = null; $("msg").value = OWN.msg; } | |
| else draw(); | |
| renderLabels(); syncMessageState(); | |
| } | |
| function draw() { | |
| const d = state.ds, prev = state.sample; | |
| let s; do { s = d.samples[Math.floor(Math.random() * d.samples.length)]; } while (d.samples.length > 1 && s === prev); | |
| state.sample = s; $("msg").value = s.t; state.edited = false; syncMessageState(); | |
| } | |
| /* ---------- labels ---------- */ | |
| function currentLabels() { // [{name, desc, send}] — `send` is the exact string sent to the API | |
| if (state.ds.key === "own") { | |
| const seen = new Set(); | |
| return ($("own-labels") ? $("own-labels").value : OWN.labels.join("\n")).split("\n").map(s => s.trim()) | |
| .filter(s => s && !seen.has(s) && seen.add(s)).map(s => ({ name: s, desc: null, send: s })); | |
| } | |
| return state.ds.labels.map(l => ({ name: l.name, desc: l.desc, send: l.desc || l.name })); | |
| } | |
| function renderLabels() { | |
| const d = state.ds; | |
| if (d.key === "own") { | |
| $("labels-area").innerHTML = `<label for="own-labels" class="sr">Candidate labels</label> | |
| <textarea id="own-labels" spellcheck="false">${esc(OWN.labels.join("\n"))}</textarea>`; | |
| $("own-labels").addEventListener("input", syncMessageState); | |
| return; | |
| } | |
| const n = d.labels.length; | |
| $("labels-area").innerHTML = `<div class="labels-sum"><div><span class="n">${n}</span> | |
| <span class="what">intents · from ${esc(d.dataset)}</span></div> | |
| <button class="linkbtn" id="toggle-labels" type="button" aria-expanded="${state.showLabels}">${state.showLabels ? "Hide" : "Show"} labels</button></div> | |
| ${state.showLabels ? `<ul class="labels-list">${d.labels.map(l => `<li><span class="nm">${esc(l.name)}</span>${l.desc | |
| ? `<span class="ds">${esc(l.desc)}</span>` : ""}</li>`).join("")}</ul>` : ""}`; | |
| $("toggle-labels").onclick = () => { state.showLabels = !state.showLabels; renderLabels(); }; | |
| } | |
| /* ---------- message state ---------- */ | |
| function syncMessageState() { | |
| const d = state.ds, own = d.key === "own"; | |
| state.edited = !own && state.sample && $("msg").value.trim() !== state.sample.t; | |
| const tag = $("msg-tag"); | |
| if (own) { tag.className = "tag edited"; tag.textContent = "Your sentence"; } | |
| else if (state.edited) { tag.className = "tag edited"; tag.textContent = "Edited — no longer a test sentence"; } | |
| else { tag.className = "tag"; tag.textContent = "Real test sentence"; } | |
| $("another").hidden = own; | |
| const n = currentLabels().length, hint = $("labels-hint"), go = $("go"); | |
| hint.className = "hint"; hint.textContent = ""; go.disabled = false; | |
| 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; } | |
| else if (own && n < 2) { hint.className = "hint warn"; hint.textContent = "Add at least 2 labels, one per line."; go.disabled = true; } | |
| else if (state.edited && n > LIVE_LIMIT) { | |
| hint.className = "hint warn"; | |
| 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”.`; | |
| go.disabled = true; | |
| } else if (!own) hint.textContent = "The jury ranks every one of these labels for the message."; | |
| } | |
| /* ---------- deciding ---------- */ | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| function isOwnDefault() { | |
| return state.ds.key === "own" && $("msg").value.trim() === OWN.msg && | |
| currentLabels().map(l => l.send).join("\n") === OWN.labels.join("\n"); | |
| } | |
| async function decide() { | |
| const labels = currentLabels(), message = $("msg").value.trim(); | |
| if (!message || $("go").disabled) return; | |
| const gold = (!state.edited && state.sample) ? state.sample.g : null; | |
| const maybeLive = state.ds.key === "own" ? !isOwnDefault() : state.edited; // everything else is pre-computed | |
| $("go").disabled = true; | |
| showDeliberating(); | |
| const timed = async () => { // the footnote reports the real response time, not the deliberation animation | |
| const t0 = performance.now(), out = await apiPredict(message, labels, maybeLive); | |
| return { ...out, secs: (performance.now() - t0) / 1000 }; | |
| }; | |
| try { | |
| const [{ res, source, secs }] = await Promise.all([timed(), sleep(350)]); | |
| showVerdict(res, labels, gold, source, secs); | |
| } catch (e) { showError(e); } | |
| finally { stopClock(); syncMessageState(); } | |
| } | |
| async function post(message, labels, timeout) { | |
| const ctrl = new AbortController(), timer = setTimeout(() => ctrl.abort(), timeout); | |
| try { | |
| const r = await fetch(API_BASE + "/predict", { method: "POST", headers: { "content-type": "application/json" }, | |
| body: JSON.stringify({ message, labels: labels.map(l => l.send) }), signal: ctrl.signal }); | |
| if (!r.ok) { | |
| let detail = ""; try { detail = (await r.json()).detail || ""; } catch (_) {} | |
| throw Object.assign(new Error(typeof detail === "string" ? detail : ""), { status: r.status }); | |
| } | |
| return { res: await r.json(), source: r.headers.get("X-Quorum-Cache") || "miss" }; | |
| } finally { clearTimeout(timer); } | |
| } | |
| async function modelAwake() { | |
| try { const r = await fetch(API_BASE + "/health", { signal: AbortSignal.timeout(5000) }); | |
| return (await r.json()).model_loaded; } catch (_) { return null; } | |
| } | |
| async function apiPredict(message, labels, maybeLive) { | |
| const timeout = maybeLive ? LIVE_TIMEOUT : HIT_TIMEOUT; | |
| if (maybeLive && (await modelAwake()) === false) showWaking(); | |
| try { return await post(message, labels, timeout); } | |
| catch (e) { | |
| // Timeouts, network errors, 503 (busy / restarting) and 504 are worth one retry: the server keeps a result it | |
| // finishes, so the second attempt is usually answered from the cache. | |
| const retriable = e.name === "AbortError" || e.name === "TypeError" || e.status === 503 || e.status === 504; | |
| if (!retriable) throw e; | |
| showWaking("Still working — trying once more…"); | |
| await sleep(3000); | |
| return await post(message, labels, timeout); | |
| } | |
| } | |
| /* ---------- rendering ---------- */ | |
| function jurorsHTML(members, verdictSend, byS) { | |
| return `<div class="jurors">${MEMBERS.map(([m, role], i) => { | |
| const mv = members && members[i]; | |
| if (!mv) return `<div class="juror thinking"><div class="who">${m}</div><div class="role">${role}</div> | |
| <div class="vote"><span class="dots"><span></span><span></span><span></span></span></div></div>`; | |
| const lab = byS.get(mv.pick), agree = mv.pick === verdictSend; | |
| return `<div class="juror"><div class="who">${m}</div><div class="role">${role}</div> | |
| <div class="vote">${esc(lab ? lab.name : mv.pick)}</div> | |
| <div class="agree ${agree ? "y" : "n"}">${agree ? "✓ with the verdict" : "≠ dissents"}</div></div>`; | |
| }).join("")}</div>`; | |
| } | |
| let clock = null; | |
| function showDeliberating() { | |
| const v = $("verdict"); v.className = "verdict card deliberating"; | |
| v.innerHTML = jurorsHTML(null) + `<div id="delib-note"><p class="hint" style="margin-top:16px">The jury is deliberating…</p></div>`; | |
| } | |
| function showWaking(headline) { | |
| const note = $("delib-note"); if (!note) return; | |
| const t0 = clock ? clock.t0 : performance.now(); | |
| note.innerHTML = `<div class="waking"><b>${esc(headline || "Waking the jury up")}</b> | |
| <p>The models sleep when the demo is quiet and load on demand, so this first answer can take a minute or two. | |
| Answers after that are quick, and anything already computed comes back instantly.</p> | |
| <div class="clock" id="clock">0 s</div></div>`; | |
| stopClock(); | |
| clock = { t0, id: setInterval(() => { const c = $("clock"); if (c) c.textContent = `${Math.round((performance.now() - t0) / 1000)} s`; }, 1000) }; | |
| } | |
| function stopClock() { if (clock) { clearInterval(clock.id); clock = null; } } | |
| function showVerdict(res, labels, gold, source, secs) { | |
| const byS = new Map(labels.map((l, i) => [l.send, { ...l, i }])); | |
| const top = byS.get(res.label), pct = res.confidence * 100; | |
| const entries = Object.entries(res.distribution); | |
| const truth = gold == null ? `<div class="truth na"><span class="ic">i</span><div>Not a test sentence, so there is no | |
| reference answer to check against.</div></div>` | |
| : gold === top.i ? `<div class="truth ok"><span class="ic">✓</span><div>Correct — this is the dataset's answer. | |
| <small>${accLine()}</small></div></div>` | |
| : `<div class="truth no"><span class="ic">✗</span><div>The dataset's answer is <b>${esc(labels[gold].name)}</b>. | |
| <small>${accLine()}</small></div></div>`; | |
| const bar = ([s, p], i) => { const l = byS.get(s); | |
| return `<div class="bar-row${i === 0 ? " top" : ""}"><div class="lab" title="${esc(l.desc || l.name)}">${esc(l.name)}${l.desc | |
| ? `<span>${esc(l.desc)}</span>` : ""}</div><div class="pct">${(p * 100).toFixed(1)}%</div> | |
| <div class="track"><div class="fill" data-w="${Math.max(1.5, p * 100)}"></div></div></div>`; }; | |
| const v = $("verdict"); v.className = "verdict card reveal"; | |
| v.innerHTML = jurorsHTML(res.members, res.label, byS) + ` | |
| <div class="ruling"><div><div class="eyebrow">The verdict</div><div class="name">${esc(top.name)}</div> | |
| ${top.desc ? `<div class="desc">${esc(top.desc)}</div>` : ""}</div> | |
| <div class="seal" aria-label="confidence ${pct.toFixed(1)} percent">${sealSVG()}<div class="num"><div> | |
| <b id="conf">0.0%</b><i>confidence</i></div></div></div></div> | |
| ${truth} | |
| <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> | |
| ${entries.length > 5 ? `<button class="linkbtn more" id="more" type="button" aria-expanded="false">Show all ${entries.length} labels</button>` : ""}</div> | |
| <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> | |
| <input type="range" id="tau" min="0" max="100" value="70" aria-label="Confidence threshold" /> | |
| <div class="decision" id="decision"></div></div> | |
| <div class="footnote"><span>${source === "hit" ? "answered from the cache" : "computed live"} in ${secs.toFixed(2)} s</span> | |
| <span>${state.ds.key === "own" ? "zero-shot · your labels" : `zero-shot · ${esc(state.ds.dataset)} · label descriptions`}</span></div>`; | |
| requestAnimationFrame(() => setTimeout(() => v.querySelectorAll(".bars > .bar-row .fill").forEach((f, i) => | |
| setTimeout(() => f.style.width = f.dataset.w + "%", matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 950 + i * 70)), 0)); | |
| countUp($("conf"), pct); | |
| if ($("more")) $("more").onclick = () => { const m = $("more-bars"), open = m.hidden; m.hidden = !open; | |
| $("more").setAttribute("aria-expanded", String(open)); $("more").textContent = open ? "Show top 5" : `Show all ${entries.length} labels`; | |
| m.querySelectorAll(".fill").forEach(f => f.style.width = open ? f.dataset.w + "%" : 0); }; | |
| const gate = () => { const t = +$("tau").value; $("tauv").textContent = t; const d = $("decision"); | |
| d.className = "decision " + (pct >= t ? "act" : "esc"); | |
| d.textContent = pct >= t ? `Auto-act — ${pct.toFixed(1)}% ≥ ${t}%` : `Escalate to a human — ${pct.toFixed(1)}% < ${t}%`; }; | |
| $("tau").oninput = gate; gate(); | |
| } | |
| function accLine() { const d = state.ds; | |
| const exposed = ["banking77", "massive", "hwu64"].includes(d.key) | |
| ? ' Not fully held out, see the <a href="https://academy.idataagent.com/research/quorum#limits-h" target="_blank" rel="noopener">research page</a>.' : ""; | |
| return `Quorum gets ${(d.accuracy * 100).toFixed(1)}% of ${d.n_test.toLocaleString()} ${esc(d.dataset)} test sentences right (zero-shot).${exposed}`; } | |
| function sealSVG() { | |
| const id = "c" + Math.random().toString(36).slice(2, 7); | |
| 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> | |
| <circle cx="60" cy="60" r="58" fill="none" stroke="var(--seal)" stroke-width="1.4"/> | |
| <circle cx="60" cy="60" r="40" fill="var(--seal-soft)" stroke="var(--seal)" stroke-width="1"/> | |
| <text font-family="IBM Plex Mono, monospace" font-size="7.4" letter-spacing="2.9" fill="var(--seal)"> | |
| <textPath href="#${id}">QUORUM · VERDICT · QUORUM · VERDICT ·</textPath></text></svg>`; | |
| } | |
| function countUp(el, target) { | |
| if (matchMedia("(prefers-reduced-motion: reduce)").matches) { el.textContent = target.toFixed(1) + "%"; return; } | |
| const t0 = performance.now() + 620, dur = 800; | |
| const tick = now => { const k = Math.min(1, Math.max(0, (now - t0) / dur)), e = 1 - Math.pow(1 - k, 3); | |
| el.textContent = (target * e).toFixed(1) + "%"; if (k < 1) requestAnimationFrame(tick); }; | |
| requestAnimationFrame(tick); | |
| } | |
| function showError(e) { | |
| const v = $("verdict"); v.className = "verdict card"; | |
| const msg = e && e.status === 422 && e.message ? e.message | |
| : "The jury couldn't answer just now. The demo runs on a small shared server, so please try again in a moment."; | |
| v.innerHTML = `<div class="err">${esc(msg)}</div>${e && e.status === 422 ? "" : | |
| `<button class="btn" id="retry" type="button">Try again<span class="arrow">→</span></button>`}`; | |
| if ($("retry")) $("retry").onclick = decide; | |
| } | |
| /* ---------- boot ---------- */ | |
| $("msg").addEventListener("input", syncMessageState); | |
| $("another").onclick = draw; | |
| $("go").onclick = decide; | |
| $("go").disabled = true; | |
| fetch("samples.json").then(r => { if (!r.ok) throw new Error(r.status); return r.json(); }).then(d => { | |
| DATA = d; renderDomains(); select("banking77"); | |
| }).catch(() => { | |
| $("verdict").innerHTML = `<div class="err">The examples could not be loaded — please refresh the page.</div>`; | |
| }); | |
| </script> | |
| </body> | |
| </html> | |