sentiment-demo / index.html
techresearchspace's picture
Upload 2 files
97390c6 verified
Raw
History Blame Contribute Delete
9.71 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sentiment Analysis — In-Browser Demo</title>
<style>
:root {
--ink: #1b1b1f;
--paper: #faf9f6;
--line: #dcd8ce;
--pos: #2f6d4f;
--pos-bg: #e6f2ea;
--neg: #9a3b3b;
--neg-bg: #f6e9e7;
--accent: #3d5a80;
--mono: "SF Mono", "Roboto Mono", ui-monospace, Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
line-height: 1.5;
}
.wrap {
max-width: 720px;
margin: 0 auto;
padding: 48px 24px 80px;
}
header {
border-bottom: 1px solid var(--line);
padding-bottom: 24px;
margin-bottom: 32px;
}
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 8px;
}
h1 {
font-size: 28px;
font-weight: 650;
margin: 0 0 8px;
letter-spacing: -0.01em;
}
.sub {
color: #55555c;
font-size: 15px;
max-width: 56ch;
}
.sub code {
font-family: var(--mono);
background: #f0eee6;
padding: 1px 5px;
border-radius: 3px;
font-size: 13px;
}
.status-row {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--mono);
font-size: 13px;
color: #77777d;
margin-top: 16px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #c9c4b6;
flex-shrink: 0;
transition: background 0.3s;
}
.dot.loading { background: #d9a441; animation: pulse 1.2s infinite ease-in-out; }
.dot.ready { background: var(--pos); }
.dot.error { background: var(--neg); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
textarea {
width: 100%;
min-height: 110px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
font-family: inherit;
font-size: 15px;
color: var(--ink);
resize: vertical;
outline: none;
transition: border-color 0.15s;
}
textarea:focus { border-color: var(--accent); }
textarea::placeholder { color: #a8a49a; }
.btn-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
}
button {
font-family: var(--mono);
font-size: 13px;
letter-spacing: 0.02em;
text-transform: uppercase;
background: var(--ink);
color: var(--paper);
border: none;
padding: 11px 20px;
border-radius: 6px;
cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
}
button:hover:not(:disabled) { opacity: 0.85; }
button:active:not(:disabled) { transform: scale(0.98); }
button:disabled { opacity: 0.35; cursor: not-allowed; }
.hint {
font-size: 12px;
color: #93908a;
}
#result {
margin-top: 28px;
}
.result-card {
border: 1px solid var(--line);
border-radius: 10px;
padding: 20px 22px;
background: #fff;
display: none;
}
.result-card.show { display: block; }
.result-top {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 14px;
}
.label-pill {
font-family: var(--mono);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 5px 12px;
border-radius: 100px;
}
.label-pill.POSITIVE { color: var(--pos); background: var(--pos-bg); }
.label-pill.NEGATIVE { color: var(--neg); background: var(--neg-bg); }
.score {
font-family: var(--mono);
font-size: 13px;
color: #77777d;
}
.bar-track {
height: 6px;
background: #eeece5;
border-radius: 100px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 100px;
transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
width: 0%;
}
.bar-fill.POSITIVE { background: var(--pos); }
.bar-fill.NEGATIVE { background: var(--neg); }
.history {
margin-top: 36px;
}
.history h2 {
font-size: 13px;
font-family: var(--mono);
text-transform: uppercase;
letter-spacing: 0.06em;
color: #93908a;
font-weight: 600;
margin-bottom: 12px;
}
.history-item {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 12px 0;
border-bottom: 1px solid var(--line);
font-size: 14px;
}
.history-item:last-child { border-bottom: none; }
.history-text {
color: #3a3a3f;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.history-label {
font-family: var(--mono);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
flex-shrink: 0;
}
.history-label.POSITIVE { color: var(--pos); }
.history-label.NEGATIVE { color: var(--neg); }
footer {
margin-top: 56px;
padding-top: 20px;
border-top: 1px solid var(--line);
font-size: 12px;
color: #a8a49a;
}
footer a { color: var(--accent); text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">In-browser inference · no server</div>
<h1>Sentiment Analysis</h1>
<p class="sub">
Runs <code>distilbert-base-uncased-finetuned-sst-2-english</code> entirely in your browser
via WebAssembly. Nothing you type is sent anywhere.
</p>
<div class="status-row">
<span class="dot loading" id="statusDot"></span>
<span id="statusText">Loading model…</span>
</div>
</header>
<textarea id="input" placeholder="I really loved this movie, the acting was fantastic!" disabled></textarea>
<div class="btn-row">
<button id="analyzeBtn" disabled>Analyze</button>
<span class="hint">⌘/Ctrl + Enter to run</span>
</div>
<div id="result">
<div class="result-card" id="resultCard">
<div class="result-top">
<span class="label-pill" id="labelPill"></span>
<span class="score" id="scoreText"></span>
</div>
<div class="bar-track">
<div class="bar-fill" id="barFill"></div>
</div>
</div>
</div>
<div class="history" id="historySection" style="display:none;">
<h2>Recent</h2>
<div id="historyList"></div>
</div>
<footer>
Model card: <a href="https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english" target="_blank" rel="noopener">distilbert-base-uncased-finetuned-sst-2-english</a>
· Powered by <a href="https://huggingface.co/docs/transformers.js" target="_blank" rel="noopener">🤗 Transformers.js</a>
</footer>
</div>
<script type="module">
import { pipeline, env } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0";
env.allowLocalModels = false;
const statusDot = document.getElementById("statusDot");
const statusText = document.getElementById("statusText");
const input = document.getElementById("input");
const analyzeBtn = document.getElementById("analyzeBtn");
const resultCard = document.getElementById("resultCard");
const labelPill = document.getElementById("labelPill");
const scoreText = document.getElementById("scoreText");
const barFill = document.getElementById("barFill");
const historySection = document.getElementById("historySection");
const historyList = document.getElementById("historyList");
let classifier = null;
const history = [];
async function loadModel() {
try {
classifier = await pipeline(
"sentiment-analysis",
"Xenova/distilbert-base-uncased-finetuned-sst-2-english"
);
statusDot.className = "dot ready";
statusText.textContent = "Model ready";
input.disabled = false;
analyzeBtn.disabled = false;
input.focus();
} catch (err) {
statusDot.className = "dot error";
statusText.textContent = "Failed to load model — check console";
console.error(err);
}
}
function renderResult(label, score) {
resultCard.classList.add("show");
labelPill.textContent = label;
labelPill.className = "label-pill " + label;
scoreText.textContent = (score * 100).toFixed(1) + "% confidence";
barFill.className = "bar-fill " + label;
requestAnimationFrame(() => {
barFill.style.width = (score * 100).toFixed(1) + "%";
});
}
function addToHistory(text, label, score) {
history.unshift({ text, label, score });
if (history.length > 6) history.pop();
historySection.style.display = "block";
historyList.innerHTML = history
.map(
(h) => `
<div class="history-item">
<span class="history-text">${escapeHtml(h.text)}</span>
<span class="history-label ${h.label}">${h.label} ${(h.score * 100).toFixed(0)}%</span>
</div>`
)
.join("");
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
async function analyze() {
const text = input.value.trim();
if (!text || !classifier) return;
analyzeBtn.disabled = true;
analyzeBtn.textContent = "Analyzing…";
const output = await classifier(text);
const { label, score } = output[0];
renderResult(label, score);
addToHistory(text, label, score);
analyzeBtn.disabled = false;
analyzeBtn.textContent = "Analyze";
}
analyzeBtn.addEventListener("click", analyze);
input.addEventListener("keydown", (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") analyze();
});
loadModel();
</script>
</body>
</html>