okcan commited on
Commit
ca3db9e
Β·
unverified Β·
0 Parent(s):

Initial commit

Browse files
Files changed (2) hide show
  1. README.md +34 -0
  2. index.html +568 -0
README.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: text-analyzer
3
+ emoji: πŸ“
4
+ colorFrom: green
5
+ colorTo: teal
6
+ sdk: static
7
+ pinned: false
8
+ license: apache-2.0
9
+ ---
10
+
11
+ # Text Readability & Tone Analyzer
12
+
13
+ Paste any text β€” blog post, essay, email, or report β€” and get instant analysis powered by the Hugging Face Inference API.
14
+
15
+ ## Features
16
+
17
+ - **Summary** β€” Condensed version of long texts via `facebook/bart-large-cnn` (skipped for texts under 100 words)
18
+ - **Tone Classification** β€” Zero-shot classification with `facebook/bart-large-mnli` across six tones: formal, casual, persuasive, informative, emotional, humorous
19
+ - **Readability Statistics** β€” Word count, sentence count, average words per sentence, syllable count, and Flesch Reading Ease score with a visual meter
20
+ - **Writing Suggestions** β€” Passive voice detection, long sentence flagging (>30 words), filler word counts
21
+
22
+ ## How to use
23
+
24
+ 1. Enter your free HF token from [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) (Read access is sufficient)
25
+ 2. Paste your text
26
+ 3. Click **Analyze Text** or press `Ctrl+Enter`
27
+
28
+ ## Models used
29
+
30
+ | Task | Model |
31
+ |------|-------|
32
+ | Summarization | `facebook/bart-large-cnn` |
33
+ | Tone classification | `facebook/bart-large-mnli` |
34
+ | Readability & suggestions | Pure JavaScript (no API) |
index.html ADDED
@@ -0,0 +1,568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.0">
6
+ <title>Text Readability & Tone Analyzer</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
11
+ background: #0f0f0f;
12
+ color: #e0e0e0;
13
+ min-height: 100vh;
14
+ padding: 32px 16px;
15
+ }
16
+ .container { max-width: 820px; margin: 0 auto; }
17
+ h1 {
18
+ font-size: 1.8rem;
19
+ font-weight: 700;
20
+ color: #fff;
21
+ margin-bottom: 6px;
22
+ }
23
+ .subtitle { color: #888; font-size: 0.95rem; margin-bottom: 32px; }
24
+
25
+ .card {
26
+ background: #1a1a1a;
27
+ border: 1px solid #2a2a2a;
28
+ border-radius: 12px;
29
+ padding: 24px;
30
+ margin-bottom: 20px;
31
+ }
32
+ label {
33
+ display: block;
34
+ font-size: 0.85rem;
35
+ color: #aaa;
36
+ margin-bottom: 8px;
37
+ font-weight: 500;
38
+ }
39
+ input, textarea {
40
+ width: 100%;
41
+ background: #111;
42
+ border: 1px solid #333;
43
+ border-radius: 8px;
44
+ color: #e0e0e0;
45
+ font-size: 0.95rem;
46
+ padding: 12px 14px;
47
+ outline: none;
48
+ transition: border-color 0.2s;
49
+ font-family: inherit;
50
+ }
51
+ input:focus, textarea:focus { border-color: #2eb87a; }
52
+ textarea { resize: vertical; min-height: 180px; line-height: 1.6; }
53
+
54
+ .token-hint { font-size: 0.78rem; color: #666; margin-top: 6px; }
55
+ .token-hint a { color: #2eb87a; text-decoration: none; }
56
+ .token-hint a:hover { text-decoration: underline; }
57
+
58
+ button {
59
+ background: #2eb87a;
60
+ color: #fff;
61
+ border: none;
62
+ border-radius: 8px;
63
+ padding: 12px 28px;
64
+ font-size: 1rem;
65
+ font-weight: 600;
66
+ cursor: pointer;
67
+ transition: background 0.2s;
68
+ width: 100%;
69
+ margin-top: 8px;
70
+ }
71
+ button:hover { background: #26a06a; }
72
+ button:disabled { background: #444; cursor: not-allowed; }
73
+
74
+ .status {
75
+ padding: 12px 16px;
76
+ border-radius: 8px;
77
+ font-size: 0.9rem;
78
+ margin-top: 12px;
79
+ display: none;
80
+ }
81
+ .status.info { background: #0e2018; color: #6ee4a8; border: 1px solid #1a4a30; display: block; }
82
+ .status.error { background: #2a1a1a; color: #f57a7a; border: 1px solid #6a2a2a; display: block; }
83
+ .status.success { background: #0e2018; color: #6ee4a8; border: 1px solid #2a6a48; display: block; }
84
+
85
+ .results { display: none; }
86
+ .results.visible { display: block; }
87
+
88
+ /* section title */
89
+ .section-title {
90
+ font-size: 0.78rem;
91
+ color: #888;
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.07em;
94
+ font-weight: 600;
95
+ margin-bottom: 14px;
96
+ }
97
+
98
+ /* ── Summary card ── */
99
+ #summaryCard { margin-bottom: 20px; }
100
+ #summaryOut { font-size: 0.97rem; line-height: 1.7; color: #ddd; }
101
+
102
+ /* ── Tone card ── */
103
+ #toneCard { margin-bottom: 20px; }
104
+ .tone-chips { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }
105
+ .tone-chip {
106
+ background: #222;
107
+ border: 1px solid #333;
108
+ border-radius: 20px;
109
+ padding: 5px 14px;
110
+ font-size: 0.85rem;
111
+ color: #bbb;
112
+ position: relative;
113
+ overflow: hidden;
114
+ min-width: 100px;
115
+ text-align: center;
116
+ }
117
+ .tone-chip .chip-fill {
118
+ position: absolute;
119
+ left: 0; top: 0; bottom: 0;
120
+ background: #2eb87a22;
121
+ border-radius: 20px;
122
+ transition: width 0.6s ease;
123
+ }
124
+ .tone-chip.top-tone {
125
+ border-color: #2eb87a;
126
+ color: #6ee4a8;
127
+ font-weight: 600;
128
+ }
129
+ .tone-chip .chip-label { position: relative; z-index: 1; }
130
+ .tone-chip .chip-pct { font-size: 0.72rem; color: #888; margin-left: 4px; position: relative; z-index: 1; }
131
+ .tone-chip.top-tone .chip-pct { color: #4cad88; }
132
+
133
+ /* ── Readability card ── */
134
+ .stats-grid {
135
+ display: grid;
136
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
137
+ gap: 12px;
138
+ margin-bottom: 20px;
139
+ }
140
+ .stat-box {
141
+ background: #111;
142
+ border: 1px solid #2a2a2a;
143
+ border-radius: 8px;
144
+ padding: 14px 16px;
145
+ }
146
+ .stat-box .stat-val {
147
+ font-size: 1.5rem;
148
+ font-weight: 700;
149
+ color: #fff;
150
+ line-height: 1;
151
+ margin-bottom: 4px;
152
+ }
153
+ .stat-box .stat-lbl { font-size: 0.78rem; color: #777; }
154
+
155
+ /* Flesch meter */
156
+ .meter-wrap { margin-top: 4px; }
157
+ .meter-label-row {
158
+ display: flex;
159
+ justify-content: space-between;
160
+ font-size: 0.82rem;
161
+ color: #888;
162
+ margin-bottom: 6px;
163
+ }
164
+ .meter-label-row b { color: #e0e0e0; }
165
+ .meter-track {
166
+ height: 10px;
167
+ border-radius: 5px;
168
+ background: #222;
169
+ overflow: hidden;
170
+ position: relative;
171
+ }
172
+ .meter-fill {
173
+ height: 100%;
174
+ border-radius: 5px;
175
+ transition: width 0.7s ease, background 0.7s ease;
176
+ }
177
+ .meter-level {
178
+ margin-top: 8px;
179
+ font-size: 0.88rem;
180
+ font-weight: 600;
181
+ }
182
+
183
+ /* ── Suggestions card ── */
184
+ .suggestion-list { list-style: none; }
185
+ .suggestion-list li {
186
+ display: flex;
187
+ align-items: flex-start;
188
+ gap: 10px;
189
+ padding: 10px 0;
190
+ border-bottom: 1px solid #222;
191
+ font-size: 0.92rem;
192
+ color: #ccc;
193
+ line-height: 1.5;
194
+ }
195
+ .suggestion-list li:last-child { border-bottom: none; }
196
+ .suggestion-list .badge {
197
+ flex-shrink: 0;
198
+ font-size: 0.72rem;
199
+ font-weight: 700;
200
+ padding: 2px 8px;
201
+ border-radius: 10px;
202
+ margin-top: 2px;
203
+ }
204
+ .badge.warn { background: #3a2800; color: #f0a832; border: 1px solid #6a4800; }
205
+ .badge.good { background: #0e2018; color: #6ee4a8; border: 1px solid #2a5a38; }
206
+ .badge.count { background: #1e1e2e; color: #9a9aff; border: 1px solid #3a3a6a; }
207
+
208
+ .spinner {
209
+ display: inline-block;
210
+ width: 14px; height: 14px;
211
+ border: 2px solid #555;
212
+ border-top-color: #6ee4a8;
213
+ border-radius: 50%;
214
+ animation: spin 0.7s linear infinite;
215
+ margin-right: 8px;
216
+ vertical-align: middle;
217
+ }
218
+ @keyframes spin { to { transform: rotate(360deg); } }
219
+
220
+ @media (max-width: 600px) {
221
+ h1 { font-size: 1.4rem; }
222
+ .stats-grid { grid-template-columns: 1fr 1fr; }
223
+ }
224
+ </style>
225
+ </head>
226
+ <body>
227
+ <div class="container">
228
+ <h1>πŸ“ Text Readability & Tone Analyzer</h1>
229
+ <p class="subtitle">Paste any text β€” get a summary, tone classification, readability score, and writing suggestions.</p>
230
+
231
+ <div class="card">
232
+ <label>HF API Token</label>
233
+ <input type="password" id="hfToken" placeholder="hf_..." autocomplete="off" />
234
+ <p class="token-hint">
235
+ Free token at <a href="https://huggingface.co/settings/tokens" target="_blank">huggingface.co/settings/tokens</a>
236
+ β€” Read access is enough. Used for summarization and tone classification.
237
+ </p>
238
+ </div>
239
+
240
+ <div class="card">
241
+ <label>Your Text</label>
242
+ <textarea id="textInput" placeholder="Paste a blog post, essay, email, report β€” anything. Minimum 20 words.&#10;&#10;Tip: longer texts produce more accurate results."></textarea>
243
+ <button id="analyzeBtn" onclick="analyze()">Analyze Text</button>
244
+ <div class="status" id="status"></div>
245
+ </div>
246
+
247
+ <!-- RESULTS -->
248
+ <div class="results" id="results">
249
+
250
+ <!-- Summary -->
251
+ <div class="card" id="summaryCard">
252
+ <div class="section-title">Summary</div>
253
+ <p id="summaryOut">β€”</p>
254
+ </div>
255
+
256
+ <!-- Tone -->
257
+ <div class="card" id="toneCard">
258
+ <div class="section-title">Tone Classification</div>
259
+ <div class="tone-chips" id="toneChips"></div>
260
+ </div>
261
+
262
+ <!-- Readability stats -->
263
+ <div class="card">
264
+ <div class="section-title">Readability Statistics</div>
265
+ <div class="stats-grid" id="statsGrid">
266
+ <div class="stat-box"><div class="stat-val" id="statWords">β€”</div><div class="stat-lbl">Words</div></div>
267
+ <div class="stat-box"><div class="stat-val" id="statSentences">β€”</div><div class="stat-lbl">Sentences</div></div>
268
+ <div class="stat-box"><div class="stat-val" id="statAvgWPS">β€”</div><div class="stat-lbl">Avg Words / Sentence</div></div>
269
+ <div class="stat-box"><div class="stat-val" id="statSyllables">β€”</div><div class="stat-lbl">Syllables</div></div>
270
+ </div>
271
+
272
+ <!-- Flesch meter -->
273
+ <div class="meter-wrap">
274
+ <div class="meter-label-row">
275
+ <span>Flesch Reading Ease</span>
276
+ <span><b id="fleschScore">β€”</b> / 100</span>
277
+ </div>
278
+ <div class="meter-track">
279
+ <div class="meter-fill" id="meterFill" style="width:0%"></div>
280
+ </div>
281
+ <div class="meter-level" id="meterLevel"></div>
282
+ </div>
283
+ </div>
284
+
285
+ <!-- Writing suggestions -->
286
+ <div class="card">
287
+ <div class="section-title">Writing Suggestions</div>
288
+ <ul class="suggestion-list" id="suggestionList"></ul>
289
+ </div>
290
+
291
+ </div><!-- /results -->
292
+ </div>
293
+
294
+ <script>
295
+ /* ───────────────────────────── helpers ───────────────────────────── */
296
+
297
+ function setStatus(msg, type) {
298
+ const el = document.getElementById('status');
299
+ el.innerHTML = msg;
300
+ el.className = 'status ' + type;
301
+ }
302
+
303
+ async function hfInference(token, model, inputs, params = {}) {
304
+ const res = await fetch(`https://api-inference.huggingface.co/models/${model}`, {
305
+ method: 'POST',
306
+ headers: {
307
+ 'Authorization': `Bearer ${token}`,
308
+ 'Content-Type': 'application/json'
309
+ },
310
+ body: JSON.stringify({ inputs, parameters: params })
311
+ });
312
+ if (!res.ok) {
313
+ const err = await res.text();
314
+ throw new Error(`HF API error ${res.status}: ${err.slice(0, 200)}`);
315
+ }
316
+ return res.json();
317
+ }
318
+
319
+ function chunkText(text, wordLimit = 800) {
320
+ const words = text.split(/\s+/);
321
+ const chunks = [];
322
+ for (let i = 0; i < words.length; i += wordLimit) {
323
+ chunks.push(words.slice(i, i + wordLimit).join(' '));
324
+ }
325
+ return chunks;
326
+ }
327
+
328
+ /* ───────────────────────────── readability (pure JS) ───────────────────────────── */
329
+
330
+ function countSyllables(word) {
331
+ word = word.toLowerCase().replace(/[^a-z]/g, '');
332
+ if (!word) return 0;
333
+ if (word.length <= 3) return 1;
334
+ // remove trailing silent e
335
+ word = word.replace(/e$/, '');
336
+ const matches = word.match(/[aeiouy]{1,2}/g);
337
+ return matches ? Math.max(1, matches.length) : 1;
338
+ }
339
+
340
+ function computeReadability(text) {
341
+ // sentences: split on . ! ?
342
+ const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
343
+ const sentenceCount = Math.max(sentences.length, 1);
344
+
345
+ // words
346
+ const words = text.match(/\b[a-zA-Z']+\b/g) || [];
347
+ const wordCount = Math.max(words.length, 1);
348
+
349
+ // syllables
350
+ let syllableCount = 0;
351
+ for (const w of words) syllableCount += countSyllables(w);
352
+
353
+ const avgWPS = wordCount / sentenceCount;
354
+ const avgSPW = syllableCount / wordCount;
355
+
356
+ // Flesch Reading Ease = 206.835 - 1.015*(words/sentences) - 84.6*(syllables/words)
357
+ let flesch = 206.835 - 1.015 * avgWPS - 84.6 * avgSPW;
358
+ flesch = Math.min(100, Math.max(0, Math.round(flesch * 10) / 10));
359
+
360
+ return { wordCount, sentenceCount, syllableCount, avgWPS: Math.round(avgWPS * 10) / 10, flesch };
361
+ }
362
+
363
+ function fleschLabel(score) {
364
+ if (score >= 90) return { label: 'Very Easy', color: '#4caf50' };
365
+ if (score >= 70) return { label: 'Easy', color: '#8bc34a' };
366
+ if (score >= 50) return { label: 'Medium', color: '#ffc107' };
367
+ if (score >= 30) return { label: 'Difficult', color: '#ff9800' };
368
+ return { label: 'Very Difficult', color: '#f44336' };
369
+ }
370
+
371
+ /* ───────────────────────────── writing suggestions (pure JS) ───────────────────────────── */
372
+
373
+ function buildSuggestions(text, stats) {
374
+ const suggestions = [];
375
+ const words = text.match(/\b[a-zA-Z']+\b/g) || [];
376
+ const wordCount = words.length;
377
+
378
+ // 1. Passive voice: was/were/been + past participle (simple heuristic)
379
+ const passivePattern = /\b(was|were|been|is|are|am|being)\s+\w+ed\b/gi;
380
+ const passiveMatches = text.match(passivePattern) || [];
381
+ const passiveCount = passiveMatches.length;
382
+ if (passiveCount === 0) {
383
+ suggestions.push({ badge: 'good', text: 'No passive voice patterns detected β€” active voice throughout.' });
384
+ } else {
385
+ suggestions.push({
386
+ badge: 'warn',
387
+ text: `Passive voice: ${passiveCount} instance${passiveCount > 1 ? 's' : ''} detected (e.g. "was done", "were written"). Consider rewriting in active voice for clarity.`
388
+ });
389
+ }
390
+
391
+ // 2. Long sentences (> 30 words)
392
+ const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
393
+ const longSentences = sentences.filter(s => s.split(/\s+/).length > 30);
394
+ const longCount = longSentences.length;
395
+ if (longCount === 0) {
396
+ suggestions.push({ badge: 'good', text: 'No overly long sentences (>30 words) found β€” great sentence variety.' });
397
+ } else {
398
+ suggestions.push({
399
+ badge: 'warn',
400
+ text: `Long sentences: ${longCount} sentence${longCount > 1 ? 's' : ''} exceed 30 words. Breaking them up can improve readability.`
401
+ });
402
+ }
403
+
404
+ // 3. Filler words
405
+ const fillers = ['very', 'really', 'just', 'quite', 'basically', 'literally', 'actually'];
406
+ let fillerCount = 0;
407
+ const foundFillers = [];
408
+ for (const filler of fillers) {
409
+ const re = new RegExp(`\\b${filler}\\b`, 'gi');
410
+ const hits = (text.match(re) || []).length;
411
+ if (hits > 0) { fillerCount += hits; foundFillers.push(`"${filler}" Γ—${hits}`); }
412
+ }
413
+ if (fillerCount === 0) {
414
+ suggestions.push({ badge: 'good', text: 'No filler words detected β€” concise and direct writing.' });
415
+ } else {
416
+ suggestions.push({
417
+ badge: 'count',
418
+ text: `Filler words: ${fillerCount} found β€” ${foundFillers.join(', ')}. Removing them tightens your prose.`
419
+ });
420
+ }
421
+
422
+ // 4. Reading level note
423
+ const { flesch } = stats;
424
+ const lvl = fleschLabel(flesch);
425
+ if (flesch >= 70) {
426
+ suggestions.push({ badge: 'good', text: `Reading level: ${lvl.label} (Flesch ${flesch}). Your text is accessible to a broad audience.` });
427
+ } else if (flesch >= 50) {
428
+ suggestions.push({ badge: 'count', text: `Reading level: ${lvl.label} (Flesch ${flesch}). Suitable for high-school/college readers; simplify where possible if targeting a general audience.` });
429
+ } else {
430
+ suggestions.push({ badge: 'warn', text: `Reading level: ${lvl.label} (Flesch ${flesch}). Text is complex β€” consider shorter sentences and simpler vocabulary.` });
431
+ }
432
+
433
+ return suggestions;
434
+ }
435
+
436
+ /* ───────────────────────────── API calls ───────────────────────────── */
437
+
438
+ async function summarize(token, text) {
439
+ const chunks = chunkText(text, 800).slice(0, 3);
440
+ const summaries = [];
441
+ for (const chunk of chunks) {
442
+ const res = await hfInference(token, 'facebook/bart-large-cnn', chunk, {
443
+ max_length: 160, min_length: 40, do_sample: false
444
+ });
445
+ if (res[0]?.summary_text) summaries.push(res[0].summary_text);
446
+ }
447
+ return summaries.join(' ') || 'Could not generate summary.';
448
+ }
449
+
450
+ async function classifyTone(token, text) {
451
+ const labels = ['formal', 'casual', 'persuasive', 'informative', 'emotional', 'humorous'];
452
+ // use first 1000 chars for zero-shot (model limit)
453
+ const snippet = text.slice(0, 1000);
454
+ const res = await hfInference(token, 'facebook/bart-large-mnli', snippet, {
455
+ candidate_labels: labels,
456
+ multi_label: false
457
+ });
458
+ // res has .labels and .scores arrays
459
+ if (!res.labels) throw new Error('Unexpected tone API response.');
460
+ return res.labels.map((l, i) => ({ label: l, score: res.scores[i] }));
461
+ }
462
+
463
+ /* ───────────────────────────── render helpers ───────────────────────────── */
464
+
465
+ function renderStats(stats) {
466
+ document.getElementById('statWords').textContent = stats.wordCount.toLocaleString();
467
+ document.getElementById('statSentences').textContent = stats.sentenceCount.toLocaleString();
468
+ document.getElementById('statAvgWPS').textContent = stats.avgWPS;
469
+ document.getElementById('statSyllables').textContent = stats.syllableCount.toLocaleString();
470
+
471
+ const lvl = fleschLabel(stats.flesch);
472
+ document.getElementById('fleschScore').textContent = stats.flesch;
473
+ const fill = document.getElementById('meterFill');
474
+ fill.style.width = stats.flesch + '%';
475
+ fill.style.background = lvl.color;
476
+ const levelEl = document.getElementById('meterLevel');
477
+ levelEl.textContent = lvl.label;
478
+ levelEl.style.color = lvl.color;
479
+ }
480
+
481
+ function renderTone(toneData) {
482
+ const container = document.getElementById('toneChips');
483
+ container.innerHTML = '';
484
+ const sorted = [...toneData].sort((a, b) => b.score - a.score);
485
+ const topLabel = sorted[0].label;
486
+ for (const item of sorted) {
487
+ const pct = Math.round(item.score * 100);
488
+ const div = document.createElement('div');
489
+ div.className = 'tone-chip' + (item.label === topLabel ? ' top-tone' : '');
490
+ div.innerHTML = `
491
+ <div class="chip-fill" style="width:${pct}%"></div>
492
+ <span class="chip-label">${item.label}</span>
493
+ <span class="chip-pct">${pct}%</span>
494
+ `;
495
+ container.appendChild(div);
496
+ }
497
+ }
498
+
499
+ function renderSuggestions(suggestions) {
500
+ const ul = document.getElementById('suggestionList');
501
+ ul.innerHTML = '';
502
+ for (const s of suggestions) {
503
+ const li = document.createElement('li');
504
+ li.innerHTML = `<span class="badge ${s.badge}">${s.badge.toUpperCase()}</span><span>${s.text}</span>`;
505
+ ul.appendChild(li);
506
+ }
507
+ }
508
+
509
+ /* ───────────────────────────── main entry ───────────────────────────── */
510
+
511
+ async function analyze() {
512
+ const token = document.getElementById('hfToken').value.trim();
513
+ const text = document.getElementById('textInput').value.trim();
514
+
515
+ if (!token) { setStatus('Please enter your HF API token.', 'error'); return; }
516
+ if (!text) { setStatus('Please paste some text to analyze.', 'error'); return; }
517
+
518
+ const wordCount = (text.match(/\b\w+\b/g) || []).length;
519
+ if (wordCount < 20) { setStatus('Text is too short β€” please provide at least 20 words.', 'error'); return; }
520
+
521
+ const btn = document.getElementById('analyzeBtn');
522
+ btn.disabled = true;
523
+ document.getElementById('results').classList.remove('visible');
524
+
525
+ try {
526
+ // 1. Readability (instant, no API)
527
+ setStatus('<span class="spinner"></span>Computing readability stats...', 'info');
528
+ const stats = computeReadability(text);
529
+ renderStats(stats);
530
+
531
+ // 2. Summary (only if > 100 words)
532
+ if (wordCount > 100) {
533
+ setStatus('<span class="spinner"></span>Generating summary (BART)...', 'info');
534
+ const summary = await summarize(token, text);
535
+ document.getElementById('summaryOut').textContent = summary;
536
+ document.getElementById('summaryCard').style.display = '';
537
+ } else {
538
+ document.getElementById('summaryOut').textContent = 'Text is under 100 words β€” summary skipped.';
539
+ document.getElementById('summaryCard').style.display = '';
540
+ }
541
+
542
+ // 3. Tone classification
543
+ setStatus('<span class="spinner"></span>Classifying tone (zero-shot)...', 'info');
544
+ const toneData = await classifyTone(token, text);
545
+ renderTone(toneData);
546
+
547
+ // 4. Suggestions
548
+ const suggestions = buildSuggestions(text, stats);
549
+ renderSuggestions(suggestions);
550
+
551
+ document.getElementById('results').classList.add('visible');
552
+ setStatus('Analysis complete.', 'success');
553
+ } catch (e) {
554
+ setStatus('Error: ' + e.message, 'error');
555
+ } finally {
556
+ btn.disabled = false;
557
+ }
558
+ }
559
+
560
+ // Allow Ctrl+Enter to submit
561
+ document.addEventListener('DOMContentLoaded', () => {
562
+ document.getElementById('textInput').addEventListener('keydown', e => {
563
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) analyze();
564
+ });
565
+ });
566
+ </script>
567
+ </body>
568
+ </html>