Spaces:
Running
Running
Upload index.html with huggingface_hub
Browse files- 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 —
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
<style>
|
| 8 |
-
:root { --ink:#1a1a2e; --muted:#6b7280; --line:#e5e7eb; --accent:#4f46e5; --
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 12 |
-
.wrap { max-width: 760px; margin: 0 auto; padding: 28px 18px
|
| 13 |
-
h1 { font-size: 1.
|
| 14 |
-
.sub { color: var(--muted); margin: 0 0
|
| 15 |
.links a { color: var(--accent); text-decoration: none; margin-right: 14px; font-size: .9rem; }
|
| 16 |
-
.
|
| 17 |
-
|
|
|
|
| 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 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
.note { font-size:.8rem; color:var(--muted); margin-top:8px; }
|
| 25 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 29 |
-
.barwrap { flex:1; background:
|
| 30 |
.pct { width: 52px; text-align:right; color: var(--muted); font-variant-numeric: tabular-nums; }
|
| 31 |
-
.members { font-size:.85rem;
|
| 32 |
.members b { color: var(--accent); }
|
| 33 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
</style>
|
| 35 |
</head>
|
| 36 |
<body>
|
| 37 |
<div class="wrap">
|
| 38 |
-
<h1>⚖️ Quorum
|
| 39 |
-
<p class="sub">
|
| 40 |
-
|
| 41 |
-
|
| 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">
|
| 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
|
| 50 |
-
<
|
|
|
|
| 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="
|
| 62 |
-
|
| 63 |
-
first request can take ~15s under load (<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="
|
| 70 |
-
<div class="
|
| 71 |
-
<
|
|
|
|
| 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 = "
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 97 |
const d = await r.json();
|
|
|
|
| 98 |
$("toplab").textContent = d.label;
|
| 99 |
-
$("topconf").textContent =
|
| 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 |
-
|
| 110 |
-
"
|
|
|
|
|
|
|
|
|
|
| 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 => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[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 |
}
|