firefox + llama.cpp == very good prose.
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>shakespeare prose.</title>
7 <style>
8 * { margin: 0; padding: 0; box-sizing: border-box; }
9
10 body {
11 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
12 background: #1a1a2e;
13 color: #e0e0e0;
14 padding: 20px;
15 min-height: 100vh;
16 }
17
18 h1 {
19 font-size: 14px;
20 font-weight: 600;
21 color: #8888aa;
22 text-transform: uppercase;
23 letter-spacing: 0.5px;
24 margin-bottom: 16px;
25 }
26
27 /* ─── Loading spinner ──────────────────────────────────────────── */
28
29 #loading {
30 display: flex;
31 flex-direction: column;
32 align-items: center;
33 justify-content: center;
34 padding: 60px 0;
35 gap: 16px;
36 }
37
38 .spinner {
39 width: 36px;
40 height: 36px;
41 border: 3px solid #333;
42 border-top-color: #6c63ff;
43 border-radius: 50%;
44 animation: spin 0.8s linear infinite;
45 }
46
47 @keyframes spin { to { transform: rotate(360deg); } }
48
49 #loading p {
50 font-size: 14px;
51 color: #8888aa;
52 }
53
54 /* ─── Error display ────────────────────────────────────────────── */
55
56 #error {
57 display: none;
58 background: #3a1a1a;
59 border: 1px solid #6b2a2a;
60 border-radius: 8px;
61 padding: 16px;
62 color: #ff8888;
63 font-size: 14px;
64 line-height: 1.5;
65 }
66
67 #error h2 {
68 font-size: 13px;
69 font-weight: 600;
70 margin-bottom: 8px;
71 color: #ff6666;
72 }
73
74 /* ─── Result display ───────────────────────────────────────────── */
75
76 #result {
77 display: none;
78 }
79
80 .section {
81 margin-bottom: 16px;
82 }
83
84 .section-label {
85 font-size: 11px;
86 font-weight: 600;
87 color: #6c63ff;
88 text-transform: uppercase;
89 letter-spacing: 0.5px;
90 margin-bottom: 8px;
91 display: inline;
92 }
93
94 .retry-icon {
95 display: none;
96 margin-left: 6px;
97 font-size: 13px;
98 cursor: pointer;
99 color: #6c63ff;
100 vertical-align: middle;
101 transition: color 0.15s, transform 0.15s;
102 }
103
104 .retry-icon:hover {
105 color: #aaffaa;
106 transform: rotate(90deg);
107 }
108
109 .retry-icon.show {
110 display: inline;
111 }
112
113 .latency {
114 display: none;
115 margin-left: 6px;
116 font-size: 10px;
117 color: #666;
118 font-family: monospace;
119 vertical-align: middle;
120 }
121
122 .latency.show {
123 display: inline;
124 }
125
126 .tokens {
127 display: none;
128 margin-left: 6px;
129 font-size: 10px;
130 color: #666;
131 font-family: monospace;
132 vertical-align: middle;
133 }
134
135 .tokens.show {
136 display: inline;
137 }
138
139 .section-error {
140 color: #ff8888;
141 font-size: 13px;
142 font-style: italic;
143 }
144
145 .section-content {
146 background: #16213e;
147 border: 1px solid #2a2a4a;
148 border-radius: 8px;
149 padding: 14px;
150 font-size: 14px;
151 line-height: 1.6;
152 white-space: pre-wrap;
153 word-break: break-word;
154 }
155
156 .section-content.original {
157 color: #aaaacc;
158 text-decoration: line-through;
159 text-decoration-color: #665566;
160 }
161
162 .section-content.corrected {
163 color: #aaffaa;
164 border-color: #2a4a2a;
165 cursor: pointer;
166 transition: border-color 0.2s, background 0.2s;
167 }
168
169 .section-content.corrected.copied {
170 border-color: #4caf50;
171 background: #1a2e1a;
172 }
173
174 .section-content.suggested {
175 color: #aaddff;
176 border-color: #2a3a5a;
177 cursor: pointer;
178 transition: border-color 0.2s, background 0.2s;
179 }
180
181 .section-content.suggested.copied {
182 border-color: #4caf50;
183 background: #1a2e2a;
184 }
185
186 /* ─── Streaming indicator (bouncing dots) ──────────────────────── */
187
188 .streaming-indicator {
189 display: inline-flex;
190 align-items: center;
191 gap: 4px;
192 padding: 4px 0;
193 }
194
195 .streaming-indicator.hidden {
196 display: none;
197 }
198
199 .streaming-indicator span {
200 display: inline-block;
201 width: 6px;
202 height: 6px;
203 border-radius: 50%;
204 background: #6c63ff;
205 animation: bounce 1.2s ease-in-out infinite;
206 }
207
208 .streaming-indicator span:nth-child(2) {
209 animation-delay: 0.15s;
210 }
211
212 .streaming-indicator span:nth-child(3) {
213 animation-delay: 0.3s;
214 }
215
216 @keyframes bounce {
217 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
218 30% { transform: translateY(-6px); opacity: 1; }
219 }
220
221 /* ─── Settings bar ─────────────────────────────────────────────── */
222
223 .settings-bar {
224 display: flex;
225 align-items: center;
226 gap: 8px;
227 padding: 8px 12px;
228 background: #16213e;
229 border: 1px solid #2a2a4a;
230 border-radius: 8px;
231 margin-bottom: 16px;
232 }
233
234 .settings-bar label {
235 font-size: 11px;
236 font-weight: 600;
237 color: #6c63ff;
238 text-transform: uppercase;
239 letter-spacing: 0.5px;
240 white-space: nowrap;
241 }
242
243 .settings-bar input {
244 flex: 1;
245 background: #1a1a2e;
246 border: 1px solid #2a2a4a;
247 border-radius: 4px;
248 padding: 4px 8px;
249 color: #e0e0e0;
250 font-size: 12px;
251 font-family: monospace;
252 outline: none;
253 transition: border-color 0.15s;
254 }
255
256 .settings-bar input:focus {
257 border-color: #6c63ff;
258 }
259
260 .checkmark {
261 opacity: 0;
262 visibility: hidden;
263 color: #4caf50;
264 font-size: 16px;
265 font-weight: 700;
266 transition: opacity 0.2s, visibility 0.2s;
267 }
268
269 .checkmark.show {
270 opacity: 1;
271 visibility: visible;
272 }
273
274 .health-dot {
275 display: none;
276 width: 8px;
277 height: 8px;
278 border-radius: 50%;
279 vertical-align: middle;
280 }
281
282 .health-dot.ok {
283 display: inline-block;
284 background: #4caf50;
285 }
286
287 .health-dot.fail {
288 display: inline-block;
289 background: #ff5555;
290 }
291
292 .lang-toggle {
293 display: flex;
294 margin-left: auto;
295 border: 1px solid #2a2a4a;
296 border-radius: 4px;
297 overflow: hidden;
298 }
299
300 .lang-btn {
301 background: #1a1a2e;
302 border: none;
303 padding: 3px 8px;
304 font-size: 11px;
305 font-weight: 600;
306 color: #888;
307 cursor: pointer;
308 transition: background 0.15s, color 0.15s;
309 }
310
311 .lang-btn + .lang-btn {
312 border-left: 1px solid #2a2a4a;
313 }
314
315 .lang-btn.active {
316 background: #6c63ff;
317 color: #fff;
318 }
319
320 .lang-btn:hover:not(.active) {
321 background: #2a2a4a;
322 color: #ccc;
323 }
324 </style>
325</head>
326<body>
327 <div class="settings-bar">
328 <label for="api-url">API</label>
329 <input type="text" id="api-url" placeholder="http://localhost:8080" spellcheck="false">
330 <span id="api-saved" class="checkmark">✓</span>
331 <span id="health-dot" class="health-dot"></span>
332 <div class="lang-toggle">
333 <button id="lang-en" class="lang-btn active" type="button">EN</button>
334 <button id="lang-fr" class="lang-btn" type="button">FR</button>
335 </div>
336 </div>
337 <div id="loading">
338 <div class="spinner"></div>
339 <p>Correcting text...</p>
340 </div>
341
342 <div id="error">
343 <h2>Error</h2>
344 <p id="error-message"></p>
345 </div>
346
347 <div id="result">
348 <div class="section">
349 <div class="section-label">Original</div>
350 <div class="section-content original" id="original-text"></div>
351 </div>
352 <div class="section">
353 <div class="section-header">
354 <span class="section-label">Corrected</span>
355 <span class="retry-icon" id="retry-corrected">↻</span>
356 <span class="latency" id="latency-corrected"></span>
357 <span class="tokens" id="tokens-corrected"></span>
358 </div>
359 <div class="section-content corrected" id="corrected-text">
360 <div id="corrected-indicator" class="streaming-indicator hidden">
361 <span></span><span></span><span></span>
362 </div>
363 </div>
364 </div>
365 <div class="section">
366 <div class="section-header">
367 <span class="section-label">Suggested</span>
368 <span class="retry-icon" id="retry-suggested">↻</span>
369 <span class="latency" id="latency-suggested"></span>
370 <span class="tokens" id="tokens-suggested"></span>
371 </div>
372 <div class="section-content suggested" id="suggested-text">
373 <div id="suggested-indicator" class="streaming-indicator hidden">
374 <span></span><span></span><span></span>
375 </div>
376 </div>
377 </div>
378 </div>
379
380 <script src="result.js"></script>
381</body>
382</html>