AnnLior commited on
Commit
958503c
·
verified ·
1 Parent(s): bb56229

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +373 -0
index.html ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Проверка русского текста (LanguageTool)</title>
7
+ <style>
8
+ * { box-sizing: border-box; }
9
+ body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; padding: 12px; background: #f4f6f8; }
10
+ h1 { font-size: 18px; margin: 0 0 10px; }
11
+ .toolbar { margin-bottom: 10px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
12
+ button { padding: 8px 12px; font-size: 14px; cursor: pointer; border: none; border-radius: 6px; background: #2563eb; color: #fff; }
13
+ button:disabled { background: #94a3b8; cursor: not-allowed; }
14
+ button.pause { background: #f59e0b; }
15
+ button.reset { background: #64748b; }
16
+ .cols { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
17
+ @media (max-width: 900px) {
18
+ .cols { grid-template-columns: 1fr; }
19
+ textarea, .output { height: 40vh; }
20
+ }
21
+ .col { background: #fff; border-radius: 8px; padding: 10px; display: flex; flex-direction: column; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
22
+ .col h2 { font-size: 13px; margin: 0 0 8px; color: #334155; text-transform: uppercase; letter-spacing: .5px; }
23
+ textarea { width: 100%; height: 55vh; resize: vertical; font-size: 14px; padding: 8px; border: 1px solid #cbd5e1; border-radius: 6px; font-family: inherit; }
24
+ .output { height: 55vh; overflow-y: auto; font-size: 14px; line-height: 1.5; padding: 8px; border: 1px solid #cbd5e1; border-radius: 6px; background: #fafafa; white-space: pre-wrap; word-wrap: break-word; }
25
+ .chunk-buttons { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
26
+ .chunk-buttons button { background: #0ea5e9; font-size: 13px; padding: 6px 10px; }
27
+ .chunk-buttons button.done { background: #16a34a; }
28
+ .chunk-buttons button.err { background: #dc2626; }
29
+ .chunk-info { font-size: 12px; color: #64748b; margin-top: 6px; }
30
+ .status { font-size: 13px; color: #475569; margin-left: 8px; }
31
+ .err { background: #fee2e2; border-bottom: 2px solid #ef4444; cursor: help; }
32
+ .fix { background: #dcfce7; color: #166534; font-weight: 600; }
33
+ .block { margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px dashed #e2e8f0; }
34
+ .block-title { font-size: 11px; color: #94a3b8; margin-bottom: 4px; }
35
+ .issue { background: #fff; border-left: 4px solid #ef4444; padding: 6px 8px; margin-bottom: 6px; border-radius: 4px; font-size: 13px; }
36
+ .issue.ok { border-color: #16a34a; }
37
+ .issue .msg { font-weight: 600; }
38
+ .issue .sugg { color: #16a34a; }
39
+ .issue .ctx { color: #64748b; font-size: 12px; margin-top: 3px; }
40
+ .issue pre { white-space: pre-wrap; word-wrap: break-word; font-size: 12px; margin: 4px 0 0; color: #7f1d1d; }
41
+ </style>
42
+ </head>
43
+ <body>
44
+
45
+ <h1>Проверка русского текста — LanguageTool</h1>
46
+
47
+ <div class="toolbar">
48
+ <button id="btnSplit">Разбить на куски ≤ 20000 знаков</button>
49
+ <button id="btnRunAll" disabled>Проверить все куски</button>
50
+ <button id="btnPause" class="pause" disabled>Пауза</button>
51
+ <button id="btnReset" class="reset">Сброс</button>
52
+ <span class="status" id="status"></span>
53
+ </div>
54
+
55
+ <div class="cols">
56
+ <div class="col">
57
+ <h2>1. Ваш текст</h2>
58
+ <textarea id="input" placeholder="Вставьте сюда большой русский текст..."></textarea>
59
+ <div class="chunk-info" id="chunkInfo"></div>
60
+ <div class="chunk-buttons" id="chunkButtons"></div>
61
+ </div>
62
+
63
+ <div class="col">
64
+ <h2>2. Результаты проверки</h2>
65
+ <div class="output" id="results">Здесь появятся ошибки по каждому куску…</div>
66
+ </div>
67
+
68
+ <div class="col">
69
+ <h2>3. Текст с подсветкой</h2>
70
+ <div class="output" id="highlighted">Здесь будет сводный текст: красным — проблемные места, зелёным — предложенные исправления.</div>
71
+ </div>
72
+ </div>
73
+
74
+ <script>
75
+ // Публичный API LanguageTool.
76
+ // Если тут будет CORS/блокировка — поменяйте на 'https://languagetool.org/api/v2/check'
77
+ const API = 'https://api.languagetool.org/v2/check';
78
+ const LIMIT = 20000;
79
+
80
+ const input = document.getElementById('input');
81
+ const results = document.getElementById('results');
82
+ const highlighted = document.getElementById('highlighted');
83
+ const chunkButtons = document.getElementById('chunkButtons');
84
+ const chunkInfo = document.getElementById('chunkInfo');
85
+ const status = document.getElementById('status');
86
+
87
+ let chunks = [];
88
+ let running = false;
89
+ let paused = false;
90
+
91
+ /* ================== СИНХРОННАЯ ПРОКРУТКА ================== */
92
+ const scrollables = [input, results, highlighted];
93
+ let syncing = false;
94
+
95
+ function syncScroll(source) {
96
+ if (syncing) return;
97
+ syncing = true;
98
+ const srcMax = source.scrollHeight - source.clientHeight;
99
+ const ratio = srcMax > 0 ? source.scrollTop / srcMax : 0;
100
+ scrollables.forEach(el => {
101
+ if (el === source) return;
102
+ const max = el.scrollHeight - el.clientHeight;
103
+ if (max > 0) el.scrollTop = ratio * max;
104
+ });
105
+ requestAnimationFrame(() => { syncing = false; });
106
+ }
107
+ scrollables.forEach(el => {
108
+ el.addEventListener('scroll', () => syncScroll(el), { passive: true });
109
+ });
110
+ /* =========================================================== */
111
+
112
+ /* ---------- Разбивка на куски ≤ LIMIT по абзацам ---------- */
113
+ function splitText(text, limit) {
114
+ const parts = [];
115
+ const paragraphs = text.split(/(\n\s*\n)/);
116
+ let buf = '';
117
+ let cursor = 0;
118
+ let bufStart = 0;
119
+
120
+ function flush() {
121
+ if (buf.length > 0) {
122
+ parts.push({ text: buf, start: bufStart, end: bufStart + buf.length, done: false, matches: [], error: null });
123
+ buf = '';
124
+ }
125
+ }
126
+
127
+ for (const p of paragraphs) {
128
+ if (p.length > limit) {
129
+ flush();
130
+ const pieces = hardSplit(p, limit);
131
+ for (const piece of pieces) {
132
+ parts.push({ text: piece, start: cursor, end: cursor + piece.length, done: false, matches: [], error: null });
133
+ cursor += piece.length;
134
+ }
135
+ bufStart = cursor;
136
+ continue;
137
+ }
138
+ if ((buf + p).length > limit) {
139
+ flush();
140
+ bufStart = cursor;
141
+ }
142
+ if (buf.length === 0) bufStart = cursor;
143
+ buf += p;
144
+ cursor += p.length;
145
+ }
146
+ flush();
147
+ return parts;
148
+ }
149
+
150
+ function hardSplit(s, limit) {
151
+ const out = [];
152
+ let i = 0;
153
+ while (i < s.length) {
154
+ let end = Math.min(i + limit, s.length);
155
+ if (end < s.length) {
156
+ const slice = s.substring(i, end);
157
+ const lastDot = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('! '), slice.lastIndexOf('? '), slice.lastIndexOf('\n'));
158
+ if (lastDot > limit * 0.5) end = i + lastDot + 1;
159
+ }
160
+ out.push(s.substring(i, end));
161
+ i = end;
162
+ }
163
+ return out;
164
+ }
165
+
166
+ document.getElementById('btnSplit').onclick = () => {
167
+ const text = input.value;
168
+ if (!text.trim()) { alert('Введите текст'); return; }
169
+ chunks = splitText(text, LIMIT);
170
+ chunkInfo.textContent = `Всего знаков: ${text.length}. Кусков: ${chunks.length} (по ≤ ${LIMIT}).`;
171
+
172
+ chunkButtons.innerHTML = '';
173
+ chunks.forEach((ch, i) => {
174
+ const b = document.createElement('button');
175
+ b.textContent = `Кусок ${i+1} (${ch.text.length} зн.)`;
176
+ b.dataset.idx = i;
177
+ b.onclick = () => runOne(i);
178
+ chunkButtons.appendChild(b);
179
+ });
180
+
181
+ document.getElementById('btnRunAll').disabled = false;
182
+ results.textContent = '';
183
+ highlighted.textContent = 'Нажмите «Проверить все куски» или отдельный кусок.';
184
+ status.textContent = '';
185
+ };
186
+
187
+ /* ---------- Запрос к LanguageTool с подробными ошибками ---------- */
188
+ async function checkChunk(text) {
189
+ const body = new URLSearchParams({ text: text, language: 'ru-RU' });
190
+ let res;
191
+ try {
192
+ res = await fetch(API, {
193
+ method: 'POST',
194
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
195
+ body
196
+ });
197
+ } catch (netErr) {
198
+ throw new Error('Сеть/CORS: ' + netErr.message +
199
+ ' — попробуйте открыть страницу через http://localhost, а не file://');
200
+ }
201
+ if (!res.ok) {
202
+ let detail = '';
203
+ try { detail = (await res.text()).substring(0, 300); } catch(_) {}
204
+ let hint = '';
205
+ if (res.status === 429) hint = ' — превышен лимит 20 запросов/мин, подождите минуту и повторите';
206
+ if (res.status === 413) hint = ' — кусок слишком большой, уменьшите LIMIT в коде';
207
+ throw new Error('HTTP ' + res.status + hint + (detail ? ' | ' + detail : ''));
208
+ }
209
+ const data = await res.json();
210
+ return data.matches || [];
211
+ }
212
+
213
+ /* ---------- Проверка одного куска ---------- */
214
+ async function runOne(idx) {
215
+ const ch = chunks[idx];
216
+ if (!ch || ch.done) return;
217
+ status.textContent = `Проверяем кусок ${idx+1}…`;
218
+ try {
219
+ ch.matches = await checkChunk(ch.text);
220
+ ch.done = true;
221
+ ch.error = null;
222
+ markButton(idx, 'done');
223
+ renderChunkResult(idx);
224
+ renderHighlighted();
225
+ status.textContent = `Кусок ${idx+1} готов (ошибок: ${ch.matches.length}).`;
226
+ return true;
227
+ } catch (e) {
228
+ console.error('Ошибка на куске', idx, e);
229
+ ch.error = e.message;
230
+ ch.done = false;
231
+ markButton(idx, 'err');
232
+ renderChunkError(idx, e.message);
233
+ status.textContent = `Ошибка на куске ${idx+1}: ${e.message}`;
234
+ return false;
235
+ }
236
+ }
237
+
238
+ function markButton(idx, cls) {
239
+ const b = chunkButtons.querySelector(`button[data-idx="${idx}"]`);
240
+ if (!b) return;
241
+ b.classList.remove('done', 'err');
242
+ if (cls) b.classList.add(cls);
243
+ }
244
+
245
+ function renderChunkResult(idx) {
246
+ const ch = chunks[idx];
247
+ const div = document.createElement('div');
248
+ div.className = 'block';
249
+ let html = `<div class="block-title">Кусок ${idx+1} (знаки ${ch.start}–${ch.end})</div>`;
250
+ if (ch.matches.length === 0) {
251
+ html += `<div class="issue ok">✅ Ошибок не найдено</div>`;
252
+ } else {
253
+ ch.matches.forEach((m, i) => {
254
+ const start = Math.max(0, m.offset - 25);
255
+ const end = Math.min(ch.text.length, m.offset + m.length + 25);
256
+ const ctx = ch.text.substring(start, end).replace(/\n/g, ' ');
257
+ const sugg = m.replacements.slice(0, 5).map(r => r.value).join(', ');
258
+ html += `<div class="issue">
259
+ <div class="msg">${i+1}. ${escapeHtml(m.message)}</div>
260
+ <div class="ctx">…${escapeHtml(ctx)}…</div>
261
+ ${sugg ? `<div class="sugg">→ ${escapeHtml(sugg)}</div>` : ''}
262
+ </div>`;
263
+ });
264
+ }
265
+ div.innerHTML = html;
266
+ results.appendChild(div);
267
+ results.scrollTop = results.scrollHeight;
268
+ }
269
+
270
+ function renderChunkError(idx, msg) {
271
+ const div = document.createElement('div');
272
+ div.className = 'block';
273
+ div.innerHTML = `<div class="block-title">Кусок ${idx+1}</div>
274
+ <div class="issue">❌ ${escapeHtml(msg)}<pre>Проверьте вкладку Console (F12) для деталей</pre></div>`;
275
+ results.appendChild(div);
276
+ results.scrollTop = results.scrollHeight;
277
+ }
278
+
279
+ /* ---------- Подсветка в 3-й колонке ---------- */
280
+ function renderHighlighted() {
281
+ let html = '';
282
+ chunks.forEach((ch, idx) => {
283
+ html += `<div class="block"><div class="block-title">Кусок ${idx+1}</div>`;
284
+ if (ch.error) {
285
+ html += `<div class="issue">❌ ${escapeHtml(ch.error)}</div>` + escapeHtml(ch.text);
286
+ } else if (!ch.done) {
287
+ html += escapeHtml(ch.text);
288
+ } else {
289
+ html += highlightChunk(ch);
290
+ }
291
+ html += `</div>`;
292
+ });
293
+ highlighted.innerHTML = html;
294
+ }
295
+
296
+ function highlightChunk(ch) {
297
+ const text = ch.text;
298
+ const matches = [...ch.matches].sort((a,b) => a.offset - b.offset);
299
+ let out = '';
300
+ let pos = 0;
301
+ for (const m of matches) {
302
+ if (m.offset < pos) continue;
303
+ out += escapeHtml(text.substring(pos, m.offset));
304
+ const orig = text.substring(m.offset, m.offset + m.length);
305
+ const sugg = m.replacements.length ? m.replacements[0].value : '';
306
+ out += `<span class="err" title="${escapeAttr(m.message)}${sugg ? ' → ' + escapeAttr(sugg) : ''}">${escapeHtml(orig)}</span>`;
307
+ if (sugg && sugg !== orig) {
308
+ out += ` <span class="fix">[→ ${escapeHtml(sugg)}]</span> `;
309
+ }
310
+ pos = m.offset + m.length;
311
+ }
312
+ out += escapeHtml(text.substring(pos));
313
+ return out;
314
+ }
315
+
316
+ /* ---------- Прогон всех кусков ---------- */
317
+ document.getElementById('btnRunAll').onclick = async () => {
318
+ if (running) return;
319
+ running = true;
320
+ paused = false;
321
+ document.getElementById('btnPause').disabled = false;
322
+ document.getElementById('btnRunAll').disabled = true;
323
+
324
+ let okCount = 0, errCount = 0;
325
+
326
+ for (let i = 0; i < chunks.length; i++) {
327
+ if (paused) {
328
+ status.textContent = `Пауза (готово: ${okCount}, ошибок: ${errCount}). Нажмите «Проверить все куски» для продолжения.`;
329
+ running = false;
330
+ document.getElementById('btnRunAll').disabled = false;
331
+ return;
332
+ }
333
+ if (chunks[i].done) { okCount++; continue; }
334
+ const ok = await runOne(i);
335
+ if (ok) okCount++; else errCount++;
336
+ if (i < chunks.length - 1) await sleep(1200);
337
+ }
338
+
339
+ if (errCount === 0) {
340
+ status.textContent = `Все куски проверены ✅ (${okCount})`;
341
+ } else {
342
+ status.textContent = `Готово: успешно ${okCount}, с ошибкой ${errCount}. Исправьте причину и повторите.`;
343
+ }
344
+ running = false;
345
+ document.getElementById('btnPause').disabled = true;
346
+ document.getElementById('btnRunAll').disabled = false;
347
+ };
348
+
349
+ document.getElementById('btnPause').onclick = () => { paused = true; };
350
+
351
+ document.getElementById('btnReset').onclick = () => {
352
+ chunks = [];
353
+ running = false; paused = false;
354
+ input.value = '';
355
+ results.textContent = '';
356
+ highlighted.textContent = '';
357
+ chunkButtons.innerHTML = '';
358
+ chunkInfo.textContent = '';
359
+ status.textContent = '';
360
+ document.getElementById('btnRunAll').disabled = true;
361
+ document.getElementById('btnPause').disabled = true;
362
+ };
363
+
364
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
365
+ function escapeHtml(s) {
366
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
367
+ }
368
+ function escapeAttr(s) {
369
+ return escapeHtml(s).replace(/"/g,'&quot;');
370
+ }
371
+ </script>
372
+ </body>
373
+ </html>