pngwn's picture
pngwn HF Staff
custom frontend: Vercel-minimal HTML/JS via @gradio/client
e6586c3 verified
Raw History Blame Contribute Delete
8.82 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GitHub Issue Classifier</title>
<style>
:root {
--ink: #0a0a0a;
--muted: #666;
--line: #eaeaea;
--bg: #fafafa;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 680px; margin: 0 auto; padding: 56px 20px 64px; }
.badge {
display: inline-block;
font-family: var(--mono); font-size: 12px; letter-spacing: .02em;
color: var(--muted);
border: 1px solid var(--line); border-radius: 999px;
padding: 4px 12px; background: #fff;
}
h1 { font-size: 32px; font-weight: 650; letter-spacing: -0.035em; margin: 16px 0 10px; }
.sub { font-size: 14px; color: var(--muted); line-height: 1.65; margin: 0; }
.sub b { color: var(--ink); font-weight: 600; }
.card {
margin-top: 32px;
background: #fff;
border: 1px solid var(--line);
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 2px rgba(0,0,0,.03);
}
textarea {
width: 100%; min-height: 128px; resize: vertical;
font-family: inherit; font-size: 14px; line-height: 1.55;
color: var(--ink);
border: 1px solid var(--line); border-radius: 10px;
padding: 12px 14px; outline: none;
transition: border-color .12s ease;
background: #fff;
}
textarea:focus { border-color: #999; }
textarea::placeholder { color: #b3b3b3; }
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 14px; flex-wrap: wrap; }
.pills { display: flex; gap: 8px; }
.pill {
font-size: 13px; color: var(--muted);
border: 1px solid var(--line); border-radius: 999px;
background: #fff; padding: 7px 14px; cursor: pointer;
transition: all .12s ease;
font-family: inherit;
}
.pill:hover { border-color: #999; color: var(--ink); }
.pill.active { background: var(--ink); color: #fff; border-color: var(--ink); }
.btn {
font-size: 14px; font-weight: 500;
background: #000; color: #fff;
border: 1px solid #000; border-radius: 8px;
padding: 10px 20px; cursor: pointer;
box-shadow: 0 1px 2px rgba(0,0,0,.18);
transition: background .12s ease;
font-family: inherit;
}
.btn:hover { background: #333; }
.btn:disabled { opacity: .55; cursor: default; }
.examples { margin-top: 14px; display: flex; flex-direction: column; gap: 6px; }
.examples-label { font-family: var(--mono); font-size: 11px; color: #999; letter-spacing: .06em; text-transform: uppercase; }
.ex {
text-align: left; font-size: 13px; color: var(--muted);
background: none; border: none; padding: 5px 0; cursor: pointer;
font-family: inherit;
border-bottom: 1px solid transparent;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ex:hover { color: var(--ink); }
.ex::before { content: "→ "; color: #b3b3b3; }
.results { margin-top: 6px; }
.results .hint { font-size: 13px; color: var(--muted); padding: 14px 2px; }
.bar-row {
display: grid; grid-template-columns: 76px 1fr 56px;
gap: 12px; align-items: center; padding: 7px 0;
}
.bar-label { font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
.bar-row.top .bar-label { color: var(--ink); font-weight: 600; }
.bar-track {
height: 10px; border-radius: 5px; background: #f1f1f1;
border: 1px solid var(--line); overflow: hidden;
}
.bar-fill { height: 100%; border-radius: 5px; background: #b8b8b8; transition: width .35s ease; }
.bar-row.top .bar-fill { background: var(--ink); }
.bar-pct { font-family: var(--mono); font-size: 12.5px; color: var(--muted); text-align: right; }
.bar-row.top .bar-pct { color: var(--ink); font-weight: 600; }
.verdict {
margin-top: 12px; padding: 12px 14px;
border: 1px solid var(--line); border-radius: 10px; background: #fff;
font-size: 13.5px; color: var(--muted);
display: none;
}
.verdict b { color: var(--ink); }
.error { color: #c62828; }
footer {
margin-top: 36px; padding-top: 16px;
border-top: 1px solid var(--line);
font-size: 12.5px; color: var(--muted); line-height: 1.8;
}
footer a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--line); }
footer a:hover { border-bottom-color: var(--ink); }
</style>
</head>
<body>
<div class="wrap">
<span class="badge">pngwn / github-issues-4class</span>
<h1>GitHub Issue Classifier</h1>
<p class="sub">Paste an issue and it gets classified as <b>bug</b>, <b>feature</b>, <b>question</b> or <b>support</b>.
Two models trained on 3,994 balanced issues — toggle between them below.</p>
<div class="card">
<textarea id="input" placeholder="[Bug]: App crashes when opening the settings page after updating to v2.3.1…"></textarea>
<div class="toolbar">
<div class="pills" id="pills">
<button class="pill active" data-model="setfit">SetFit · 22M</button>
<button class="pill" data-model="distilbert">DistilBERT · 66M</button>
</div>
<button class="btn" id="go">Classify</button>
</div>
<div class="results" id="results">
<div class="hint">Results appear here.</div>
</div>
<div class="examples">
<span class="examples-label">Try one</span>
<button class="ex">[Bug]: App crashes when opening the settings page after updating to v2.3.1. Stack trace attached — happens on every launch.</button>
<button class="ex">Feature request: add keyboard shortcuts for switching between editor tabs, like VSCode has.</button>
<button class="ex">How do I configure a custom API endpoint? The docs only cover the default one and I keep getting auth errors.</button>
<button class="ex">I need help getting the CLI to work on Windows — after a clean install it says 'command not found'. Any ideas?</button>
</div>
</div>
<footer>
Models: <a href="https://huggingface.co/pngwn/github-issue-classifier-setfit-minilm">SetFit MiniLM (22M)</a>
· <a href="https://huggingface.co/pngwn/github-issue-classifier-distilbert">DistilBERT (66M)</a>
— Dataset: <a href="https://huggingface.co/datasets/pngwn/github-issues-4class">github-issues-4class</a>.
SetFit: 0.807 macro-F1 · DistilBERT: 0.794 on the held-out test split.
</footer>
</div>
<script type="module">
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
const ta = document.getElementById("input");
const go = document.getElementById("go");
const results = document.getElementById("results");
const pills = document.getElementById("pills");
let model = "setfit";
const client = await Client.connect(window.location.origin);
pills.addEventListener("click", (e) => {
const b = e.target.closest(".pill");
if (!b) return;
model = b.dataset.model;
pills.querySelectorAll(".pill").forEach((p) => p.classList.toggle("active", p === b));
});
document.querySelectorAll(".ex").forEach((b) =>
b.addEventListener("click", () => { ta.value = b.textContent; classify(); })
);
go.addEventListener("click", classify);
ta.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) classify(); });
function render(data) {
const top = data.probs.indexOf(Math.max(...data.probs));
const rows = data.labels
.map((label, i) => {
const pct = data.probs[i] * 100;
return `<div class="bar-row${i === top ? " top" : ""}">
<div class="bar-label">${label}</div>
<div class="bar-track"><div class="bar-fill" style="width:${pct.toFixed(1)}%"></div></div>
<div class="bar-pct">${pct.toFixed(1)}%</div>
</div>`;
})
.join("");
results.innerHTML =
`<div class="bars">${rows}</div>
<div class="verdict" style="display:block">Predicted class: <b>${data.labels[top]}</b> (${(data.probs[top] * 100).toFixed(1)}% confidence)</div>`;
}
async function classify() {
const text = ta.value.trim();
if (!text) return;
go.disabled = true;
go.textContent = "Classifying…";
results.innerHTML = '<div class="hint">Running…</div>';
try {
const result = await client.predict("/classify", { text, model });
render(result.data[0]);
} catch (err) {
results.innerHTML = `<div class="hint error">Something went wrong: ${String(err.message || err)}</div>`;
} finally {
go.disabled = false;
go.textContent = "Classify";
}
}
</script>
</body>
</html>