| <!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> |
|
|