hub-task-tagger / static /index.html
davanstrien's picture
davanstrien HF Staff
Accept/reject feedback: per-tag votes stored in a private bucket with a pseudonymous id
5605c42 verified
Raw History Blame Contribute Delete
14.6 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dataset tagger</title>
<link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
<style>
:root{--bg:#F7F8FA;--ink:#1D2330;--muted:#6B7280;--rule:#E1E4EA;--bar:#3450C4;--bar-soft:#E3E7F6;--owner:#8A5F0E;--owner-soft:#F5EBD6;--panel:#EEF1F5}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 "Schibsted Grotesk",system-ui,sans-serif}
main{max-width:980px;margin:0 auto;padding:52px 24px 72px}
h1{font-weight:700;font-size:36px;line-height:1.1;letter-spacing:-.015em;margin:0 0 8px}
.lede{color:var(--muted);max-width:62ch;margin:0 0 26px}
form{display:flex;gap:8px;max-width:680px;background:#fff;border:1px solid #C9CED8;border-radius:10px;padding:6px}
input{flex:1;border:0;background:transparent;font:500 18px "Schibsted Grotesk",sans-serif;padding:8px 10px;color:var(--ink);min-width:0}
input:focus{outline:none}
form:focus-within{border-color:var(--bar);box-shadow:0 0 0 3px var(--bar-soft)}
button{font:600 15px "Schibsted Grotesk",sans-serif;border:0;border-radius:7px;background:var(--ink);color:#fff;cursor:pointer;padding:0 18px}
button:focus-visible,a:focus-visible{outline:2px solid var(--bar);outline-offset:3px}
#go,button.secondary{border:1px solid var(--ink)}button.secondary{background:#fff;color:var(--ink)}
.examples{margin:10px 0 42px;color:var(--muted);font-size:14px}
.examples button{font:14px "Schibsted Grotesk",sans-serif;background:none;color:var(--ink);text-decoration:underline;text-decoration-color:#C9CED8;text-underline-offset:3px;margin-right:14px;padding:0;border-radius:0}
.grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:44px;align-items:start}
h2{font-size:14px;font-weight:600;margin:0 0 12px;color:var(--muted)}
.dsname{font-size:21px;font-weight:600;margin:0 0 2px;word-break:break-all}
.meta{font-size:14px;margin:0 0 24px}
.meta a,.about a{color:var(--bar);text-underline-offset:3px}
.row{display:grid;grid-template-columns:minmax(0,1fr) 130px 52px;align-items:center;gap:14px;padding:6px 0}
.row .name{color:var(--muted)}
.row.set .name{color:var(--ink);font-weight:600}
.row .track{height:6px;background:var(--bar-soft);border-radius:3px;position:relative;overflow:hidden}
.row .bar{position:absolute;inset:0 auto 0 0;background:var(--bar);border-radius:3px;opacity:.35}
.row.set .bar{opacity:1}
.row .p{text-align:right;font-variant-numeric:tabular-nums;color:var(--muted);font-size:14px}
.row.set .p{color:var(--ink);font-weight:600}
#tags.fb .row{grid-template-columns:minmax(0,1fr) 130px 52px 58px}
.fbt{display:flex;gap:4px;justify-content:flex-end}
.fbt button{font:14px/1 "Schibsted Grotesk",sans-serif;width:26px;height:26px;padding:0;background:none;color:var(--muted);border:1px solid var(--rule);border-radius:6px}
.fbt button[aria-pressed=true].yes{color:var(--bar);border-color:var(--bar);background:var(--bar-soft)}
.fbt button[aria-pressed=true].no{color:#B04A2F;border-color:#B04A2F;background:#F8E6E1}
.fbbar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin:12px 0 0;font-size:13px;color:var(--muted)}.fbbar p{margin:0}.fbbar[hidden]{display:none}
.fbbar button{font-size:14px;padding:6px 14px}
.tag-note{grid-column:1 / -1;font-size:13px;margin:-2px 0 4px;color:var(--muted)}
.owner-mark{display:inline-block;background:var(--owner-soft);color:var(--owner);font-size:12px;font-weight:500;border-radius:999px;padding:1px 8px;margin-left:8px;vertical-align:1px}
.owner{margin-top:20px;font-size:14px;color:var(--muted)}
.owner small{font-size:12px;color:var(--muted)}
.owner span{color:var(--owner);font-weight:500}
.reads{font:13px/1.7 "JetBrains Mono",ui-monospace,monospace;white-space:pre-wrap;word-break:break-word;color:#2A3140;background:var(--panel);border-radius:10px;padding:16px 18px;max-height:420px;overflow:auto;margin:0}
.caption{color:var(--muted);font-size:13px;margin:8px 0 0}
.viewer{margin-top:48px}
.viewer iframe{width:100%;height:520px;border:1px solid var(--rule);border-radius:10px;background:#fff}
.about{margin-top:52px;max-width:64ch;font-size:15px;color:#3A4150;border-top:1px solid var(--rule);padding-top:20px}.about p{margin:0 0 10px}.todo{color:#B04A2F;font-style:italic}
.timing{margin-top:18px;color:var(--muted);font-size:13px}
.status{margin:-28px 0 42px;font-size:15px;max-width:680px}.status:empty{display:none}.status.err{color:#B04A2F}.status.load{color:var(--muted)}button:disabled{opacity:.6;cursor:default}
@media (max-width:760px){.grid{grid-template-columns:1fr;gap:34px}.row{grid-template-columns:minmax(0,1fr) 80px 48px}#tags.fb .row{grid-template-columns:minmax(0,1fr) 56px 44px 58px;gap:10px}}
@media (max-width:560px){#tags.fb .row{grid-template-columns:minmax(0,1fr) 36px 42px 58px}form{flex-wrap:wrap}input{flex:1 1 100%}form button{flex:1 1 100%;padding:11px 18px}}
</style>
</head>
<body>
<main>
<h1>What is this dataset for?</h1>
<p class="lede">Paste a Hugging Face dataset id. A small fine-tuned model reads the column names and the first row, then suggests task tags.</p>
<form id="f"><input id="q" aria-label="Dataset id" placeholder="owner/dataset" autocomplete="off"><button type="button" id="random" class="secondary">Random dataset</button><button id="go">Suggest tags</button></form>
<div class="examples">Examples: <span id="ex"></span></div>
<p class="status" id="status" role="status" aria-live="polite"></p>
<section id="out" hidden>
<div class="grid">
<div>
<p class="dsname" id="name"></p>
<p class="meta"><a id="card" target="_blank" rel="noopener">Open the dataset card</a></p>
<h2>Suggested tags</h2>
<div id="tags"></div>
<div class="fbbar" id="fbbar" hidden><button type="button" class="secondary" id="fbsend" hidden>Send feedback</button><p id="fbnote">Feedback is stored with a pseudonymous id to evaluate and improve the model.</p></div>
<p class="owner">Owner's tags: <span id="owner"></span><br><small>Set by whoever uploaded the dataset, so often incomplete.</small></p>
<p class="timing" id="timing"></p>
</div>
<div>
<h2>What the model reads</h2>
<pre class="reads" id="reads"></pre>
<p class="caption">Column names and types, then the first row, cut to fit the model's input.</p>
</div>
</div>
<div class="viewer">
<h2>First rows</h2>
<iframe id="viewer" title="Dataset viewer" loading="lazy"></iframe>
</div>
</section>
<section class="about">
<h2>About the model</h2>
<p>TypeSafe AI's <a href="https://docs.typesafe.ai/introduction" target="_blank" rel="noopener">Jev</a> has got people excited about "System One" models: models that don't generate text, but read a state and return typed answers with probabilities that software can use directly. This page does the same thing for one narrow job. The state is a dataset's columns and first row. The answer is a set of task tags, each with a probability. The model is small enough to fine-tune yourself on Jobs and to run on a free CPU.</p>
<p>The tags come from <a href="https://huggingface.co/davanstrien/hub-task-tagger-gliner2.5-base" target="_blank" rel="noopener">GLiNER2.5-base</a>, a 194M-parameter model from <a href="https://huggingface.co/fastino/gliner2.5-base-v1" target="_blank" rel="noopener">Fastino</a> that reads a text and a list of labels together. It was fine-tuned on 16,000 Hub datasets and their owners' tags, in 17 minutes on one GPU with <a href="https://huggingface.co/datasets/uv-scripts/classification/blob/main/train-gliner2.py" target="_blank" rel="noopener">this training script</a>.</p>
<p>On 3,000 newer datasets from owners it never saw, its first suggestion matches an owner's tag 69% of the time. That is a noisy target: owners often leave out tags that fit, and in a hand-checked sample about one dataset in ten was missing one. A suggestion that isn't in the owner's tags is not always wrong.</p>
</section>
</main>
<script>
const EXAMPLES = ["hubxrt/LPMusicCapsMTT_a2t", "dougdotcon/douvras-failure-atlas", "mim-chess-vlas/train_800_dense__mask__overlay_a75__sim__agentview_camera__static"];
const $ = id => document.getElementById(id);
function pct(p){ return p < 0.001 ? "<0.1%" : (p*100).toFixed(p < 0.1 ? 1 : 0) + "%"; }
function status(text, kind){ $("status").textContent = text; $("status").className = "status " + (kind || ""); }
function show(ex){
$("out").hidden = false; $("q").value = ex.dataset_id;
$("name").textContent = ex.dataset_id;
$("card").href = "https://huggingface.co/datasets/" + ex.dataset_id;
const declared = new Set(ex.declared), inSet = new Set(ex.set);
$("tags").innerHTML = ex.top.map(t => {
const set = inSet.has(t.tag), own = declared.has(t.tag);
const mark = own ? `<span class="owner-mark">owner's tag</span>` : "";
const note = set && !own ? `<p class="tag-note">Not in the owner's tags.</p>` : "";
const toggles = ex.feedback ? `<span class="fbt"><button type="button" class="yes" data-tag="${t.tag}" aria-pressed="false" aria-label="${t.tag} fits" title="Fits">✓</button><button type="button" class="no" data-tag="${t.tag}" aria-pressed="false" aria-label="${t.tag} does not fit" title="Does not fit">✗</button></span>` : "";
return `<div class="row ${set ? "set" : ""}"><span class="name">${t.tag}${mark}</span><span class="track"><span class="bar" style="width:${Math.max(t.p*100,0.8)}%"></span></span><span class="p">${pct(t.p)}</span>${toggles}${note}</div>`;
}).join("");
$("tags").classList.toggle("fb", !!ex.feedback);
current = ex; votes = {}; sent = false;
$("fbbar").hidden = !ex.feedback; $("fbsend").hidden = true; $("fbsend").disabled = false;
$("fbnote").textContent = "Feedback is stored with a pseudonymous id to evaluate and improve the model.";
$("owner").textContent = ex.declared.length ? ex.declared.join(", ") : "none";
$("reads").textContent = ex.state;
const viewer = `https://huggingface.co/datasets/${ex.dataset_id}/embed/viewer`;
if ($("viewer").src !== viewer) $("viewer").src = viewer;
$("timing").textContent = ex.timing.cached ? "Served from cache."
: `Fetched the preview in ${ex.timing.fetch_ms} ms. The model took ${ex.timing.model_ms} ms on a free CPU.`;
}
let pending = null;
// Feedback on the shown result: votes[tag] = "yes" | "no"; one submission per shown result.
let current = null, votes = {}, sent = false;
$("tags").onclick = e => {
const b = e.target.closest(".fbt button");
if (!b || sent) return;
const tag = b.dataset.tag, v = b.classList.contains("yes") ? "yes" : "no";
votes[tag] = votes[tag] === v ? undefined : v;
for (const x of $("tags").querySelectorAll(`.fbt button[data-tag="${tag}"]`))
x.setAttribute("aria-pressed", String(votes[tag] === (x.classList.contains("yes") ? "yes" : "no")));
$("fbsend").hidden = !Object.values(votes).some(Boolean);
};
$("fbsend").onclick = async () => {
if (sent || !current) return;
const tags = Object.keys(votes);
const body = {
dataset_id: current.dataset_id, sha: current.sha,
model_repo: current.model.repo, model_revision: current.model.revision, T: current.model.T, tau: current.model.tau,
suggested: current.top, set: current.set,
accepted: tags.filter(t => votes[t] === "yes"), rejected: tags.filter(t => votes[t] === "no"),
owner_tags: current.declared, ts: new Date().toISOString(),
};
$("fbsend").disabled = true;
try {
const r = await fetch("api/feedback", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)});
let reply = null;
try { reply = await r.json(); } catch (_) {}
if (!r.ok) {
$("fbnote").textContent = reply && reply.message ? reply.message : `Could not send the feedback (status ${r.status}).`;
$("fbsend").disabled = r.status === 400; // a retry cannot fix invalid feedback
return;
}
sent = true;
$("fbsend").hidden = true;
for (const x of $("tags").querySelectorAll(".fbt button")) x.disabled = true;
$("fbnote").textContent = "Thanks. Your feedback was saved.";
} catch (e) {
$("fbnote").textContent = "Could not reach the server. Try again.";
$("fbsend").disabled = false;
}
};
function busy(on){ $("go").disabled = on; $("random").disabled = on; }
async function run(id){
id = id.trim();
if (!id) { $("out").hidden = true; status("Paste a dataset id first, for example " + EXAMPLES[0] + ".", "err"); return; }
if (pending) pending.abort();
pending = new AbortController();
busy(true); $("out").hidden = true;
status("Reading the dataset…", "load");
try {
const r = await fetch("api/predict", {method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({dataset_id: id}), signal: pending.signal});
let body = null;
try { body = await r.json(); } catch (_) {}
if (!r.ok) {
status(body && body.message ? body.message
: `Something went wrong on the server (status ${r.status}). Try again in a minute.`, "err");
return;
}
status("");
show(body);
} catch (e) {
if (e.name === "AbortError") return;
status("Could not reach the server. Check your connection and try again.", "err");
} finally {
busy(false);
}
}
$("ex").innerHTML = EXAMPLES.map((id,i) => `<button type="button" data-i="${i}">${id.length > 40 ? id.slice(0,38) + "…" : id}</button>`).join("");
$("ex").onclick = e => { const i = e.target.dataset.i; if (i !== undefined) { $("q").value = EXAMPLES[i]; run(EXAMPLES[i]); } };
$("random").onclick = async () => {
if (pending) pending.abort();
busy(true); $("out").hidden = true;
status("Picking a random dataset…", "load");
try {
const r = await fetch("api/random");
let body = null;
try { body = await r.json(); } catch (_) {}
if (!r.ok) {
status(body && body.message ? body.message
: `Could not pick a random dataset (status ${r.status}). Try one of the examples.`, "err");
return;
}
$("q").value = body.dataset_id;
await run(body.dataset_id); // run() keeps both buttons disabled until the prediction arrives
} catch (e) {
status("Could not reach the server. Check your connection and try again.", "err");
} finally {
busy(false);
}
};
$("f").onsubmit = e => { e.preventDefault(); run($("q").value); };
</script>
</body>
</html>