Download code/web/index.html from nonsodev/essay-coach: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/nonsodev/essay-coach/resolve/main/code/web/index.html
- Command line
-
hf download hf://nonsodev/essay-coach/code/web/index.html
-
curl -L -o index.html https://huggingface.co/nonsodev/essay-coach/resolve/main/code/web/index.html
11.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Essay coach</title> | |
| <style> | |
| :root { | |
| --bg: #f6f1e7; --card: #fffaf0; --ink: #1d1a16; --muted: #6d655a; --line: #e2d8c6; | |
| --accent: #b5532a; --unsure: #d9a441; --chip: #efe5d3; --good: #4f8a5b; --bad: #b5532a; | |
| --lead: #e9d8a6; --position: #f2c4a8; --claim: #cfe3f2; --counterclaim: #e6cdee; --rebuttal: #f5d6de; --evidence: #d4ecd0; --concluding: #e9d8a6; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root:not([data-theme="light"]) { | |
| --bg: #15130f; --card: #1f1c17; --ink: #f1ead9; --muted: #a49a89; --line: #343026; | |
| --accent: #e0794a; --unsure: #e2b356; --chip: #2a261f; --good: #7fc08b; --bad: #e0794a; | |
| --lead: #4a4020; --position: #553424; --claim: #223b4d; --counterclaim: #3e2a46; --rebuttal: #4b2a33; --evidence: #26402a; --concluding: #4a4020; | |
| } | |
| } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; background: var(--bg); color: var(--ink); font: 17px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; } | |
| main { max-width: 820px; margin: 0 auto; padding: 32px 16px 64px; } | |
| h1 { font-size: 30px; margin: 0 0 4px; letter-spacing: -0.01em; } | |
| .sub { color: var(--muted); margin: 0 0 24px; } | |
| .card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px; } | |
| textarea { width: 100%; min-height: 220px; resize: vertical; border: 0; outline: 0; background: transparent; color: var(--ink); font: inherit; font-size: 17px; line-height: 1.6; } | |
| .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 12px; } | |
| button, .chip { font: inherit; font-size: 14px; border: 1px solid var(--line); background: var(--chip); color: var(--ink); border-radius: 999px; padding: 6px 12px; cursor: pointer; } | |
| button.primary { background: var(--accent); border-color: var(--accent); color: #fff; } | |
| button:disabled { opacity: .6; cursor: default; } | |
| .meta { color: var(--muted); font-size: 13px; margin-left: auto; } | |
| .label { font-size: 13px; color: var(--muted); margin: 22px 0 6px; } | |
| .hidden { display: none; } | |
| .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } | |
| @media (max-width: 640px) { .grid { grid-template-columns: 1fr; } } | |
| .band { display: flex; align-items: baseline; gap: 12px; } | |
| .band b { font-size: 48px; line-height: 1; letter-spacing: -0.02em; } | |
| .band small { color: var(--muted); font-size: 13px; } | |
| .skills { display: grid; gap: 8px; } | |
| .skill { display: grid; grid-template-columns: 110px 1fr 36px; gap: 10px; align-items: center; font-size: 14px; } | |
| .bar { height: 8px; background: var(--chip); border-radius: 999px; overflow: hidden; } | |
| .bar i { display: block; height: 100%; background: var(--accent); border-radius: 999px; transition: width .5s; } | |
| .essay { white-space: pre-wrap; line-height: 1.8; font-size: 16px; } | |
| .part { border-radius: 4px; padding: 1px 2px; cursor: default; position: relative; } | |
| .part.ineffective { text-decoration: underline wavy var(--bad); text-decoration-thickness: 1.5px; text-underline-offset: 3px; } | |
| .part.effective { box-shadow: inset 0 -2px 0 var(--good); } | |
| .part:hover::after { content: attr(data-tip); position: absolute; left: 0; top: 100%; z-index: 4; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: 4px 8px; font-size: 12px; white-space: nowrap; box-shadow: 0 6px 18px rgba(0,0,0,.15); } | |
| .legend { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; } | |
| .legend span { font-size: 12px; padding: 2px 8px; border-radius: 999px; } | |
| .notes { margin: 0; padding: 0; list-style: none; display: grid; gap: 8px; } | |
| .notes li { padding: 10px 12px; border-radius: 10px; background: var(--chip); font-size: 15px; } | |
| .notes li.part-note { cursor: pointer; } | |
| .notes li.part-note:hover { outline: 1px solid var(--accent); } | |
| .foot { color: var(--muted); font-size: 13px; margin-top: 28px; line-height: 1.6; } | |
| .flash { animation: flash 1.2s; } | |
| @keyframes flash { 0% { background: var(--accent); color: #fff; } 100% { background: inherit; } } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <h1>Essay coach</h1> | |
| <p class="sub">Paste an essay. It comes back scored on six skills, with every part of your argument marked and the weak spots called out.</p> | |
| <div class="card"> | |
| <textarea id="in" placeholder="Paste your essay here. A Task 2 style essay works best: a question, your position, reasons, the other side, a conclusion." spellcheck="true"></textarea> | |
| <div class="row"> | |
| <button class="primary" id="go">Read my essay</button> | |
| <button class="chip" id="sample">Try a sample</button> | |
| <span class="meta" id="meta"></span> | |
| </div> | |
| </div> | |
| <div id="result" class="hidden"> | |
| <div class="grid" style="margin-top:22px"> | |
| <div class="card"> | |
| <div class="label" style="margin-top:0">Band estimate</div> | |
| <div class="band"><b id="band">–</b><small id="bandnote"></small></div> | |
| </div> | |
| <div class="card"> | |
| <div class="label" style="margin-top:0">Six skills, out of 5</div> | |
| <div class="skills" id="skills"></div> | |
| </div> | |
| </div> | |
| <div class="label">What to fix first</div> | |
| <div class="card"><ul class="notes" id="notes"></ul></div> | |
| <div class="label">Your argument, part by part</div> | |
| <div class="card"> | |
| <div class="essay" id="essay"></div> | |
| <div class="legend" id="legend"></div> | |
| </div> | |
| </div> | |
| <p class="foot">Scores come from a model trained on 6,500 learner essays rated by human teachers (ELLIPSE) and 25,000 essays with every argument part marked (PERSUADE 2.0). The band is an estimate fitted on 1,200 human-banded IELTS essays, shown with its own error rate. Nothing you paste is stored.</p> | |
| </main> | |
| <script> | |
| const DEFAULT_API = "https://sylvayoutube--essay-coach-server-read.us-east.modal.run"; | |
| const API = new URLSearchParams(location.search).get("api") || window.ESSAY_API || DEFAULT_API; | |
| const $in = document.getElementById("in"), $go = document.getElementById("go"), $meta = document.getElementById("meta"); | |
| const TYPES = ["Lead", "Position", "Claim", "Counterclaim", "Rebuttal", "Evidence", "Concluding Statement"]; | |
| const COLOR = { "Lead": "--lead", "Position": "--position", "Claim": "--claim", "Counterclaim": "--counterclaim", "Rebuttal": "--rebuttal", "Evidence": "--evidence", "Concluding Statement": "--concluding" }; | |
| const SKILL = { cohesion: "Cohesion", syntax: "Syntax", vocabulary: "Vocabulary", phraseology: "Phrasing", grammar: "Grammar", conventions: "Conventions" }; | |
| const SAMPLE = `Some people believe that university students should study whatever they like, while others think they should only study subjects that will be useful in the future, such as science and technology. In my opinion, students should be free to choose, but universities must make the consequences of each choice clear. | |
| The main argument for free choice is motivation. A student who is forced into engineering because it is "useful" will often do the minimum, while a student who chose history because she loves it will read beyond the course and think harder. In my own secondary school, the best results in the final exams came from students who had picked their subjects themselves, not from those whose parents chose for them. | |
| Supporters of the other view say that a country needs doctors and programmers more than it needs philosophers, and that public money should go where the jobs are. This is a fair point, especially in countries where university places are paid for by the government. However, it assumes that we can predict which skills will matter in twenty years, and the last twenty years suggest we cannot. Many of today's most valuable jobs did not exist when current graduates started school, and a narrow science-only policy would have missed them entirely. | |
| In conclusion, I believe students should choose freely, because motivation produces better graduates than instruction does. Universities should publish honest information about where each course leads, including how many graduates find work and what they earn, and then leave the decision to the student.`; | |
| document.getElementById("sample").onclick = () => { $in.value = SAMPLE; }; | |
| $go.onclick = read; | |
| $in.addEventListener("keydown", (e) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") read(); }); | |
| async function read() { | |
| const text = $in.value.trim(); | |
| if (text.split(/\s+/).length < 20) { $meta.textContent = "paste at least a few sentences"; return; } | |
| if (!API) { $meta.textContent = "no API configured (add ?api=<url>)"; return; } | |
| $go.disabled = true; $meta.textContent = "reading…"; | |
| try { | |
| const r = await fetch(API, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text }) }); | |
| const d = await r.json(); | |
| if (d.error) { $meta.textContent = d.error; return; } | |
| render(text, d); | |
| $meta.textContent = `${d.ms} ms`; | |
| } catch (err) { | |
| $meta.textContent = "could not reach the coach"; | |
| } finally { $go.disabled = false; } | |
| } | |
| function render(text, d) { | |
| document.getElementById("result").classList.remove("hidden"); | |
| // band | |
| const $b = document.getElementById("band"), $bn = document.getElementById("bandnote"); | |
| if (d.band) { $b.textContent = d.band.value.toFixed(1); $bn.textContent = d.band.note; } | |
| else { $b.textContent = "–"; $bn.textContent = "band model not fitted yet"; } | |
| // skills | |
| const $s = document.getElementById("skills"); $s.innerHTML = ""; | |
| for (const k of Object.keys(SKILL)) { | |
| const v = d.scores[k]; | |
| const row = document.createElement("div"); row.className = "skill"; | |
| row.innerHTML = `<span>${SKILL[k]}</span><div class="bar"><i style="width:${((v - 1) / 4) * 100}%"></i></div><span>${v.toFixed(1)}</span>`; | |
| $s.append(row); | |
| } | |
| // essay with parts | |
| const $e = document.getElementById("essay"); $e.innerHTML = ""; | |
| let pos = 0; | |
| const parts = [...d.parts].sort((a, b) => a.start - b.start); | |
| for (const p of parts) { | |
| if (p.start < pos) continue; | |
| if (p.start > pos) $e.append(text.slice(pos, p.start)); | |
| const span = document.createElement("span"); | |
| span.className = `part ${p.effectiveness.toLowerCase()}`; | |
| span.style.background = `var(${COLOR[p.type]})`; | |
| span.dataset.tip = `${p.type} · ${p.effectiveness.toLowerCase()} (${Math.round(Math.max(...p.eff_probs) * 100)}%)`; | |
| span.dataset.start = p.start; | |
| span.textContent = text.slice(p.start, p.end); | |
| $e.append(span); | |
| pos = p.end; | |
| } | |
| if (pos < text.length) $e.append(text.slice(pos)); | |
| const $l = document.getElementById("legend"); $l.innerHTML = ""; | |
| for (const t of TYPES) { | |
| if (!parts.some(p => p.type === t)) continue; | |
| const s = document.createElement("span"); s.style.background = `var(${COLOR[t]})`; s.textContent = t; $l.append(s); | |
| } | |
| // notes | |
| const $n = document.getElementById("notes"); $n.innerHTML = ""; | |
| for (const n of d.notes) { | |
| const li = document.createElement("li"); li.textContent = n.text; | |
| if (n.start != null) { | |
| li.className = "part-note"; | |
| li.onclick = () => { const el = $e.querySelector(`[data-start="${n.start}"]`); if (el) { el.scrollIntoView({ block: "center", behavior: "smooth" }); el.classList.remove("flash"); void el.offsetWidth; el.classList.add("flash"); } }; | |
| } | |
| $n.append(li); | |
| } | |
| if (!d.notes.length) { const li = document.createElement("li"); li.textContent = "Nothing stands out. Strong structure, every claim backed."; $n.append(li); } | |
| } | |
| </script> | |
| </body> | |
| </html> | |