abersbail commited on
Commit
5ff3ceb
·
verified ·
1 Parent(s): 42d6b23

Switch to static browser sentiment app

Browse files
Files changed (2) hide show
  1. README.md +8 -7
  2. index.html +456 -0
README.md CHANGED
@@ -1,21 +1,22 @@
1
  ---
2
  title: Sentiment Analysis
3
- sdk: gradio
4
- app_file: app.py
5
  pinned: false
6
  license: apache-2.0
7
  models:
8
- - distilbert-base-uncased-finetuned-sst-2-english
9
  tags:
10
  - sentiment-analysis
11
  - text-classification
12
- - gradio
13
  ---
14
 
15
  # Sentiment Analysis
16
 
17
- A small Gradio web interface for sentiment analysis using
18
- `distilbert-base-uncased-finetuned-sst-2-english`.
 
19
 
20
  Enter a review, message, comment, or social post and the app returns a positive
21
- or negative prediction with confidence scores.
 
 
1
  ---
2
  title: Sentiment Analysis
3
+ sdk: static
 
4
  pinned: false
5
  license: apache-2.0
6
  models:
7
+ - Xenova/distilbert-base-uncased-finetuned-sst-2-english
8
  tags:
9
  - sentiment-analysis
10
  - text-classification
11
+ - transformers.js
12
  ---
13
 
14
  # Sentiment Analysis
15
 
16
+ A static web interface for sentiment analysis using
17
+ `Xenova/distilbert-base-uncased-finetuned-sst-2-english` through
18
+ Transformers.js.
19
 
20
  Enter a review, message, comment, or social post and the app returns a positive
21
+ or negative prediction with confidence scores. The model runs in the browser, so
22
+ the Space does not need paid or limited Hugging Face runtime hardware.
index.html ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Sentiment Analysis</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ --bg: #f6f7f4;
11
+ --panel: #ffffff;
12
+ --ink: #18211f;
13
+ --muted: #64706c;
14
+ --line: #d9ded9;
15
+ --accent: #087f74;
16
+ --accent-strong: #045e56;
17
+ --positive: #16825d;
18
+ --negative: #b54343;
19
+ --shadow: 0 18px 45px rgba(27, 39, 35, 0.1);
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ min-height: 100vh;
29
+ background:
30
+ linear-gradient(135deg, rgba(8, 127, 116, 0.08), transparent 42%),
31
+ var(--bg);
32
+ color: var(--ink);
33
+ font-family:
34
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
35
+ "Segoe UI", sans-serif;
36
+ }
37
+
38
+ main {
39
+ width: min(1120px, calc(100% - 32px));
40
+ margin: 0 auto;
41
+ padding: 42px 0;
42
+ }
43
+
44
+ header {
45
+ display: grid;
46
+ gap: 10px;
47
+ margin-bottom: 28px;
48
+ }
49
+
50
+ h1 {
51
+ margin: 0;
52
+ font-size: clamp(2.15rem, 5vw, 4.4rem);
53
+ line-height: 0.95;
54
+ letter-spacing: 0;
55
+ }
56
+
57
+ header p {
58
+ max-width: 720px;
59
+ margin: 0;
60
+ color: var(--muted);
61
+ font-size: 1.05rem;
62
+ line-height: 1.6;
63
+ }
64
+
65
+ .workspace {
66
+ display: grid;
67
+ grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
68
+ gap: 18px;
69
+ align-items: stretch;
70
+ }
71
+
72
+ .panel {
73
+ background: var(--panel);
74
+ border: 1px solid var(--line);
75
+ border-radius: 8px;
76
+ box-shadow: var(--shadow);
77
+ }
78
+
79
+ .input-panel,
80
+ .result-panel {
81
+ padding: 20px;
82
+ }
83
+
84
+ label {
85
+ display: block;
86
+ margin-bottom: 10px;
87
+ color: #26312e;
88
+ font-weight: 700;
89
+ }
90
+
91
+ textarea {
92
+ width: 100%;
93
+ min-height: 230px;
94
+ resize: vertical;
95
+ border: 1px solid #c7cec9;
96
+ border-radius: 8px;
97
+ padding: 16px;
98
+ color: var(--ink);
99
+ font: inherit;
100
+ line-height: 1.5;
101
+ outline: none;
102
+ background: #fbfcfb;
103
+ }
104
+
105
+ textarea:focus {
106
+ border-color: var(--accent);
107
+ box-shadow: 0 0 0 3px rgba(8, 127, 116, 0.16);
108
+ }
109
+
110
+ .actions {
111
+ display: flex;
112
+ flex-wrap: wrap;
113
+ gap: 10px;
114
+ margin-top: 14px;
115
+ }
116
+
117
+ button {
118
+ min-height: 42px;
119
+ border: 1px solid transparent;
120
+ border-radius: 8px;
121
+ padding: 0 16px;
122
+ cursor: pointer;
123
+ font: inherit;
124
+ font-weight: 700;
125
+ }
126
+
127
+ .primary {
128
+ background: var(--accent);
129
+ color: white;
130
+ }
131
+
132
+ .primary:hover {
133
+ background: var(--accent-strong);
134
+ }
135
+
136
+ .secondary {
137
+ background: #eef2ef;
138
+ border-color: #d4dbd6;
139
+ color: #25312e;
140
+ }
141
+
142
+ .secondary:hover {
143
+ background: #e4eae6;
144
+ }
145
+
146
+ .examples {
147
+ display: grid;
148
+ gap: 8px;
149
+ margin-top: 18px;
150
+ }
151
+
152
+ .example-row {
153
+ display: flex;
154
+ flex-wrap: wrap;
155
+ gap: 8px;
156
+ }
157
+
158
+ .example {
159
+ min-height: 34px;
160
+ border-color: #cdd6d1;
161
+ background: #fbfcfb;
162
+ color: #33413d;
163
+ font-size: 0.92rem;
164
+ font-weight: 650;
165
+ }
166
+
167
+ .result-panel {
168
+ display: grid;
169
+ gap: 18px;
170
+ align-content: start;
171
+ }
172
+
173
+ .status {
174
+ min-height: 24px;
175
+ color: var(--muted);
176
+ font-weight: 650;
177
+ }
178
+
179
+ .verdict {
180
+ display: grid;
181
+ gap: 8px;
182
+ padding: 18px;
183
+ border-radius: 8px;
184
+ background: #f4f7f5;
185
+ border: 1px solid #dce3de;
186
+ }
187
+
188
+ .verdict span {
189
+ color: var(--muted);
190
+ font-size: 0.9rem;
191
+ font-weight: 700;
192
+ text-transform: uppercase;
193
+ }
194
+
195
+ .verdict strong {
196
+ font-size: 2rem;
197
+ line-height: 1.05;
198
+ }
199
+
200
+ .scores {
201
+ display: grid;
202
+ gap: 14px;
203
+ }
204
+
205
+ .score-row {
206
+ display: grid;
207
+ gap: 7px;
208
+ }
209
+
210
+ .score-head {
211
+ display: flex;
212
+ justify-content: space-between;
213
+ gap: 16px;
214
+ color: #2a3834;
215
+ font-weight: 700;
216
+ }
217
+
218
+ .track {
219
+ height: 12px;
220
+ overflow: hidden;
221
+ border-radius: 999px;
222
+ background: #edf1ee;
223
+ }
224
+
225
+ .bar {
226
+ width: 0;
227
+ height: 100%;
228
+ border-radius: inherit;
229
+ transition: width 240ms ease;
230
+ }
231
+
232
+ .bar.positive {
233
+ background: var(--positive);
234
+ }
235
+
236
+ .bar.negative {
237
+ background: var(--negative);
238
+ }
239
+
240
+ .model-note {
241
+ margin: 0;
242
+ color: var(--muted);
243
+ font-size: 0.92rem;
244
+ line-height: 1.5;
245
+ }
246
+
247
+ @media (max-width: 820px) {
248
+ main {
249
+ width: min(100% - 24px, 640px);
250
+ padding: 26px 0;
251
+ }
252
+
253
+ .workspace {
254
+ grid-template-columns: 1fr;
255
+ }
256
+
257
+ textarea {
258
+ min-height: 190px;
259
+ }
260
+ }
261
+ </style>
262
+ </head>
263
+ <body>
264
+ <main>
265
+ <header>
266
+ <h1>Sentiment Analysis</h1>
267
+ <p>
268
+ Analyze a review, message, comment, or social post with a small
269
+ Hugging Face sentiment model that runs directly in your browser.
270
+ </p>
271
+ </header>
272
+
273
+ <section class="workspace" aria-label="Sentiment analysis workspace">
274
+ <div class="panel input-panel">
275
+ <label for="text-input">Text</label>
276
+ <textarea
277
+ id="text-input"
278
+ placeholder="Type a review, message, comment, or social post..."
279
+ ></textarea>
280
+
281
+ <div class="actions">
282
+ <button class="primary" id="analyze-button">Analyze</button>
283
+ <button class="secondary" id="clear-button">Clear</button>
284
+ </div>
285
+
286
+ <div class="examples">
287
+ <label>Examples</label>
288
+ <div class="example-row">
289
+ <button class="example" data-example="The product arrived early and works better than I expected.">
290
+ Positive review
291
+ </button>
292
+ <button class="example" data-example="The update made the app slow and frustrating to use.">
293
+ Negative feedback
294
+ </button>
295
+ <button class="example" data-example="The movie was okay, but the ending felt rushed.">
296
+ Mixed comment
297
+ </button>
298
+ </div>
299
+ </div>
300
+ </div>
301
+
302
+ <aside class="panel result-panel" aria-label="Results">
303
+ <div class="status" id="status">Model is ready to load.</div>
304
+
305
+ <div class="verdict">
306
+ <span>Prediction</span>
307
+ <strong id="verdict">Waiting</strong>
308
+ </div>
309
+
310
+ <div class="scores">
311
+ <div class="score-row">
312
+ <div class="score-head">
313
+ <span>Positive</span>
314
+ <span id="positive-score">0%</span>
315
+ </div>
316
+ <div class="track">
317
+ <div class="bar positive" id="positive-bar"></div>
318
+ </div>
319
+ </div>
320
+
321
+ <div class="score-row">
322
+ <div class="score-head">
323
+ <span>Negative</span>
324
+ <span id="negative-score">0%</span>
325
+ </div>
326
+ <div class="track">
327
+ <div class="bar negative" id="negative-bar"></div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <p class="model-note">
333
+ Model: Xenova/distilbert-base-uncased-finetuned-sst-2-english.
334
+ The first run downloads the model once, then your browser caches it.
335
+ </p>
336
+ </aside>
337
+ </section>
338
+ </main>
339
+
340
+ <script type="module">
341
+ import { env, pipeline } from "https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2";
342
+
343
+ env.allowLocalModels = false;
344
+
345
+ const modelId = "Xenova/distilbert-base-uncased-finetuned-sst-2-english";
346
+ const textInput = document.querySelector("#text-input");
347
+ const analyzeButton = document.querySelector("#analyze-button");
348
+ const clearButton = document.querySelector("#clear-button");
349
+ const statusEl = document.querySelector("#status");
350
+ const verdictEl = document.querySelector("#verdict");
351
+ const positiveScoreEl = document.querySelector("#positive-score");
352
+ const negativeScoreEl = document.querySelector("#negative-score");
353
+ const positiveBarEl = document.querySelector("#positive-bar");
354
+ const negativeBarEl = document.querySelector("#negative-bar");
355
+
356
+ let classifierPromise;
357
+
358
+ function setStatus(message) {
359
+ statusEl.textContent = message;
360
+ }
361
+
362
+ function setBusy(isBusy) {
363
+ analyzeButton.disabled = isBusy;
364
+ clearButton.disabled = isBusy;
365
+ analyzeButton.textContent = isBusy ? "Analyzing..." : "Analyze";
366
+ }
367
+
368
+ function updateScores(positive, negative) {
369
+ const positivePct = Math.round(positive * 100);
370
+ const negativePct = Math.round(negative * 100);
371
+ positiveScoreEl.textContent = `${positivePct}%`;
372
+ negativeScoreEl.textContent = `${negativePct}%`;
373
+ positiveBarEl.style.width = `${positivePct}%`;
374
+ negativeBarEl.style.width = `${negativePct}%`;
375
+ verdictEl.textContent =
376
+ positive >= negative ? "Positive" : "Negative";
377
+ }
378
+
379
+ async function getClassifier() {
380
+ if (!classifierPromise) {
381
+ setStatus("Loading model...");
382
+ classifierPromise = pipeline("sentiment-analysis", modelId);
383
+ }
384
+ return classifierPromise;
385
+ }
386
+
387
+ function normalizeResults(output) {
388
+ const rows = Array.isArray(output?.[0]) ? output[0] : output;
389
+ const scores = { positive: 0, negative: 0 };
390
+ for (const row of rows) {
391
+ const label = String(row.label || "").toLowerCase();
392
+ if (label.includes("positive")) scores.positive = row.score;
393
+ if (label.includes("negative")) scores.negative = row.score;
394
+ }
395
+ if (!scores.positive && !scores.negative && rows[0]) {
396
+ const first = rows[0];
397
+ if (String(first.label).toLowerCase().includes("positive")) {
398
+ scores.positive = first.score;
399
+ scores.negative = 1 - first.score;
400
+ } else {
401
+ scores.negative = first.score;
402
+ scores.positive = 1 - first.score;
403
+ }
404
+ }
405
+ return scores;
406
+ }
407
+
408
+ async function analyze() {
409
+ const text = textInput.value.trim();
410
+ if (!text) {
411
+ setStatus("Enter text first.");
412
+ verdictEl.textContent = "Waiting";
413
+ updateScores(0, 0);
414
+ return;
415
+ }
416
+
417
+ setBusy(true);
418
+ try {
419
+ const classifier = await getClassifier();
420
+ setStatus("Running sentiment analysis...");
421
+ const output = await classifier(text, { topk: null });
422
+ const scores = normalizeResults(output);
423
+ updateScores(scores.positive, scores.negative);
424
+ setStatus("Analysis complete.");
425
+ } catch (error) {
426
+ console.error(error);
427
+ setStatus("Model could not load. Refresh and try again.");
428
+ } finally {
429
+ setBusy(false);
430
+ }
431
+ }
432
+
433
+ analyzeButton.addEventListener("click", analyze);
434
+ textInput.addEventListener("keydown", (event) => {
435
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
436
+ analyze();
437
+ }
438
+ });
439
+
440
+ clearButton.addEventListener("click", () => {
441
+ textInput.value = "";
442
+ verdictEl.textContent = "Waiting";
443
+ updateScores(0, 0);
444
+ setStatus("Model is ready to load.");
445
+ textInput.focus();
446
+ });
447
+
448
+ document.querySelectorAll("[data-example]").forEach((button) => {
449
+ button.addEventListener("click", () => {
450
+ textInput.value = button.dataset.example;
451
+ analyze();
452
+ });
453
+ });
454
+ </script>
455
+ </body>
456
+ </html>