moodring / index.html
usmar's picture
Upload folder using huggingface_hub
8a62f41 verified
Raw
History Blame Contribute Delete
8.43 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>moodring — emotion classification</title>
<style>
:root {
--bg: #fcfcfb; --card: #ffffff; --ink: #2b2b2a; --muted: #6e6e6b;
--line: #e4e4e1; --blue: #2a78d6; --orange: #eb6834;
--blue-soft: #e8f0fb; --track: #f0f0ee;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a19; --card: #232322; --ink: #e8e8e5; --muted: #9c9c98;
--line: #3a3a38; --blue: #3987e5; --orange: #d95926;
--blue-soft: #1e2f44; --track: #2e2e2c;
}
}
* { box-sizing: border-box; margin: 0; }
body {
background: var(--bg); color: var(--ink);
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex; justify-content: center; padding: 28px 16px;
}
main { width: 100%; max-width: 780px; }
header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 4px; }
h1 { font-size: 22px; letter-spacing: -0.02em; }
h1 .ring { color: var(--blue); }
.tag { color: var(--muted); font-size: 13px; }
p.sub { color: var(--muted); font-size: 13.5px; margin-bottom: 18px; }
.card {
background: var(--card); border: 1px solid var(--line); border-radius: 10px;
padding: 16px; margin-bottom: 14px;
}
textarea {
width: 100%; min-height: 84px; resize: vertical; border: 1px solid var(--line);
border-radius: 8px; padding: 10px 12px; font: inherit; color: var(--ink);
background: var(--bg); outline: none;
}
textarea:focus { border-color: var(--blue); }
.row { display: flex; gap: 10px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
button {
font: inherit; font-size: 14px; font-weight: 600; border-radius: 8px;
padding: 8px 18px; cursor: pointer; border: 1px solid transparent;
}
#analyze { background: var(--blue); color: #fff; }
#analyze:disabled { opacity: 0.5; cursor: default; }
#sampleBtn { background: none; border-color: var(--line); color: var(--ink); }
#sampleBtn:hover { border-color: var(--blue); }
.meta { color: var(--muted); font-size: 12.5px; margin-left: auto; }
#result { display: none; }
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.chip {
background: var(--blue-soft); color: var(--blue); border-radius: 999px;
font-size: 13px; font-weight: 650; padding: 3px 12px;
}
.chip.gold { background: none; border: 1px dashed var(--line); color: var(--muted); font-weight: 500; }
.note { color: var(--muted); font-size: 12.5px; margin-bottom: 12px; }
.note .warn { color: var(--orange); font-weight: 600; }
.bars { display: grid; grid-template-columns: 110px 1fr 46px; gap: 6px 10px; align-items: center; }
.bl { font-size: 13px; color: var(--ink); text-align: right; }
.bl.dim { color: var(--muted); }
.track { position: relative; height: 12px; background: var(--track); border-radius: 4px; }
.fill { position: absolute; inset: 0 auto 0 0; border-radius: 4px; background: var(--blue); min-width: 2px; }
.fill.dim { opacity: 0.35; }
.tick { position: absolute; top: -2px; bottom: -2px; width: 2px; background: var(--orange); border-radius: 1px; }
.bv { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.legend { display: flex; gap: 16px; margin-top: 12px; color: var(--muted); font-size: 12px; align-items: center; }
.sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; background: var(--blue); margin-right: 5px; vertical-align: -1px; }
.swt { display: inline-block; width: 2px; height: 12px; background: var(--orange); margin-right: 6px; vertical-align: -2px; }
footer { color: var(--muted); font-size: 12px; margin-top: 4px; }
footer a { color: var(--blue); text-decoration: none; }
</style>
</head>
<body>
<script src="./shim.js"></script>
<main>
<header><h1>mood<span class="ring">ring</span></h1><span class="tag">multi-label emotion classification</span></header>
<p class="sub">28 emotions from the GoEmotions taxonomy, scored by a 30k-feature linear model.
Type something, or pull a real Reddit comment from the held-out test split.</p>
<div class="card">
<textarea id="text" placeholder="Type a sentence — e.g. “Thanks so much, this made my whole week!”"></textarea>
<div class="row">
<button id="analyze">Analyze</button>
<button id="sampleBtn">Try a sample</button>
<span class="meta" id="meta"></span>
</div>
</div>
<div class="card" id="result">
<div class="chips" id="chips"></div>
<div class="note" id="note"></div>
<div class="bars" id="bars"></div>
<div class="legend">
<span><span class="sw"></span>model score</span>
<span><span class="swt"></span>decision threshold (per label, tuned on dev)</span>
</div>
</div>
<footer>TF-IDF (1–2 grams) + one-vs-rest logistic regression · macro-F1 0.44 on the GoEmotions test split ·
<a href="https://github.com/UsmarHaider/moodring">source</a></footer>
</main>
<script>
const $ = id => document.getElementById(id);
let gold = null, totalSamples = 5427;
function setBusy(b) { $('analyze').disabled = b; }
async function analyze() {
const text = $('text').value.trim();
if (!text) return;
setBusy(true);
const t0 = performance.now();
try {
const res = await fetch('/predict', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text}),
});
if (!res.ok) throw new Error('predict failed: ' + res.status);
render(await res.json(), performance.now() - t0);
} catch (e) {
$('meta').textContent = e.message;
} finally { setBusy(false); }
}
function render(out, ms) {
const chips = $('chips');
chips.innerHTML = '';
for (const label of out.labels) {
const c = document.createElement('span');
c.className = 'chip'; c.textContent = label; chips.appendChild(c);
}
if (gold) {
for (const g of gold) {
const c = document.createElement('span');
c.className = 'chip gold'; c.textContent = 'gold: ' + g; chips.appendChild(c);
}
}
$('note').innerHTML = out.fallback
? '<span class="warn">no label cleared its threshold</span> — showing the top-scoring emotion instead'
: 'labels whose score cleared their per-label threshold';
const entries = Object.entries(out.scores).sort((a, b) => b[1] - a[1]).slice(0, 10);
const bars = $('bars');
bars.innerHTML = '';
const max = Math.max(entries[0][1], ...entries.map(e => out.thresholds[e[0]]), 0.55);
for (const [label, score] of entries) {
const picked = out.labels.includes(label);
const bl = document.createElement('div');
bl.className = 'bl' + (picked ? '' : ' dim'); bl.textContent = label;
const track = document.createElement('div');
track.className = 'track';
const fill = document.createElement('div');
fill.className = 'fill' + (picked ? '' : ' dim');
fill.style.width = (100 * score / max) + '%';
const tick = document.createElement('div');
tick.className = 'tick';
tick.style.left = 'calc(' + (100 * out.thresholds[label] / max) + '% - 1px)';
track.append(fill, tick);
const bv = document.createElement('div');
bv.className = 'bv'; bv.textContent = score.toFixed(2);
bars.append(bl, track, bv);
}
$('result').style.display = 'block';
$('meta').textContent = ms.toFixed(0) + ' ms';
}
async function loadSample(index) {
const i = index ?? Math.floor(Math.random() * totalSamples);
try {
const res = await fetch('/sample?index=' + i);
if (!res.ok) throw new Error('samples unavailable on this host');
const s = await res.json();
totalSamples = s.total;
$('text').value = s.text;
gold = s.gold;
await analyze();
$('meta').textContent += ' · test example #' + s.index;
} catch (e) { $('meta').textContent = e.message; }
}
$('analyze').addEventListener('click', () => { gold = null; analyze(); });
$('sampleBtn').addEventListener('click', () => loadSample());
$('text').addEventListener('keydown', e => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { gold = null; analyze(); }
});
const params = new URLSearchParams(location.search);
if (params.get('demo')) {
const idx = params.get('sample');
if (idx !== null) loadSample(parseInt(idx, 10));
else { $('text').value = 'Thanks so much, this made my whole week!'; analyze(); }
}
</script>
</body>
</html>