File size: 11,465 Bytes
e1679fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
<!doctype html>
<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>