AnnLior commited on
Commit
c8e35ef
·
verified ·
1 Parent(s): ee0f2e8

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +49 -6
index.html CHANGED
@@ -73,6 +73,11 @@
73
  .block-title { font-family: -apple-system, Segoe UI, Roboto, sans-serif; font-size: 11px; color: #94a3b8; margin-bottom: 4px; }
74
  .issue { background: #fff; border-left: 4px solid #ef4444; padding: 6px 8px; margin-bottom: 6px; border-radius: 4px; font-size: 13px; }
75
  .issue.ok { border-color: #16a34a; }
 
 
 
 
 
76
  .issue .msg { font-weight: 600; }
77
  .issue .sugg { color: #16a34a; }
78
  .issue .ctx { color: #64748b; font-size: 12px; margin-top: 3px; }
@@ -541,7 +546,7 @@ function renderChunkResult(idx) {
541
  const end = Math.min(ch.text.length, m.offset + m.length + 25);
542
  const ctx = ch.text.substring(start, end).replace(/\n/g, ' ');
543
  const sugg = m.replacements.slice(0, 5).map(r => r.value).join(', ');
544
- html += `<div class="issue">
545
  <div class="msg">${i+1}. ${escapeHtml(m.message)}</div>
546
  <div class="ctx">…${escapeHtml(ctx)}…</div>
547
  ${sugg ? `<div class="sugg">→ ${escapeHtml(sugg)}</div>` : ''}
@@ -573,7 +578,7 @@ function renderHighlighted() {
573
  } else if (!ch.done) {
574
  html += fmtSpan(ch.start, ch.end);
575
  } else {
576
- html += highlightChunk(ch);
577
  }
578
  html += `</div>`;
579
  });
@@ -581,18 +586,18 @@ function renderHighlighted() {
581
  highlighted.scrollTop = keepScroll;
582
  }
583
 
584
- function highlightChunk(ch) {
585
  const text = ch.text;
586
  const g = ch.start;
587
- const matches = [...ch.matches].sort((a,b) => a.offset - b.offset);
588
  let out = '';
589
  let pos = 0;
590
- for (const m of matches) {
591
  if (m.offset < pos) continue;
592
  out += fmtSpan(g + pos, g + m.offset);
593
  const orig = text.substring(m.offset, m.offset + m.length);
594
  const sugg = m.replacements.length ? m.replacements[0].value : '';
595
- out += `<span class="err" title="${escapeAttr(m.message)}${sugg ? ' → ' + escapeAttr(sugg) : ''}">${fmtSpan(g + m.offset, g + m.offset + m.length)}</span>`;
596
  if (sugg && sugg !== orig) {
597
  out += ` <span class="fix">[→ ${escapeHtml(sugg)}]</span> `;
598
  }
@@ -602,6 +607,44 @@ function highlightChunk(ch) {
602
  return out;
603
  }
604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  /* ---------- Прогон всех кусков ---------- */
606
  document.getElementById('btnRunAll').onclick = async () => {
607
  if (running) return;
 
73
  .block-title { font-family: -apple-system, Segoe UI, Roboto, sans-serif; font-size: 11px; color: #94a3b8; margin-bottom: 4px; }
74
  .issue { background: #fff; border-left: 4px solid #ef4444; padding: 6px 8px; margin-bottom: 6px; border-radius: 4px; font-size: 13px; }
75
  .issue.ok { border-color: #16a34a; }
76
+ .issue[data-m] { cursor: pointer; transition: background .15s; }
77
+ .issue[data-m]:hover { background: #f1f5f9; }
78
+ .issue.active { background: #fef9c3; border-left-color: #f59e0b; }
79
+ @keyframes flash { 0%,100% { box-shadow: 0 0 0 0 rgba(245,158,11,0); } 30% { box-shadow: 0 0 0 4px rgba(245,158,11,.7); background: #fde68a; } }
80
+ .err.flash { animation: flash 1.6s ease-out; }
81
  .issue .msg { font-weight: 600; }
82
  .issue .sugg { color: #16a34a; }
83
  .issue .ctx { color: #64748b; font-size: 12px; margin-top: 3px; }
 
546
  const end = Math.min(ch.text.length, m.offset + m.length + 25);
547
  const ctx = ch.text.substring(start, end).replace(/\n/g, ' ');
548
  const sugg = m.replacements.slice(0, 5).map(r => r.value).join(', ');
549
+ html += `<div class="issue" data-chunk="${idx}" data-m="${i}" title="Показать в тексте">
550
  <div class="msg">${i+1}. ${escapeHtml(m.message)}</div>
551
  <div class="ctx">…${escapeHtml(ctx)}…</div>
552
  ${sugg ? `<div class="sugg">→ ${escapeHtml(sugg)}</div>` : ''}
 
578
  } else if (!ch.done) {
579
  html += fmtSpan(ch.start, ch.end);
580
  } else {
581
+ html += highlightChunk(ch, idx);
582
  }
583
  html += `</div>`;
584
  });
 
586
  highlighted.scrollTop = keepScroll;
587
  }
588
 
589
+ function highlightChunk(ch, ci) {
590
  const text = ch.text;
591
  const g = ch.start;
592
+ const matches = ch.matches.map((m, i) => ({ m, i })).sort((a, b) => a.m.offset - b.m.offset);
593
  let out = '';
594
  let pos = 0;
595
+ for (const { m, i: mi } of matches) {
596
  if (m.offset < pos) continue;
597
  out += fmtSpan(g + pos, g + m.offset);
598
  const orig = text.substring(m.offset, m.offset + m.length);
599
  const sugg = m.replacements.length ? m.replacements[0].value : '';
600
+ out += `<span class="err" data-mid="${ci}-${mi}" title="${escapeAttr(m.message)}${sugg ? ' → ' + escapeAttr(sugg) : ''}">${fmtSpan(g + m.offset, g + m.offset + m.length)}</span>`;
601
  if (sugg && sugg !== orig) {
602
  out += ` <span class="fix">[→ ${escapeHtml(sugg)}]</span> `;
603
  }
 
607
  return out;
608
  }
609
 
610
+ /* ---------- Клик по ошибке во 2-й колонке → переход к месту в тексте ---------- */
611
+ results.addEventListener('click', e => {
612
+ const issue = e.target.closest('.issue[data-m]');
613
+ if (!issue) return;
614
+ jumpToMatch(+issue.dataset.chunk, +issue.dataset.m, issue);
615
+ });
616
+
617
+ function jumpToMatch(ci, mi, issueEl) {
618
+ const ch = chunks[ci];
619
+ const m = ch && ch.matches[mi];
620
+ if (!m) return;
621
+
622
+ results.querySelectorAll('.issue.active').forEach(x => x.classList.remove('active'));
623
+ if (issueEl) issueEl.classList.add('active');
624
+
625
+ syncing = true; // не даём синхронной прокрутке сдвинуть остальные колонки
626
+
627
+ // 3-я колонка: точно к нужному месту
628
+ const el = highlighted.querySelector(`[data-mid="${ci}-${mi}"]`);
629
+ if (el) {
630
+ const hr = highlighted.getBoundingClientRect();
631
+ const er = el.getBoundingClientRect();
632
+ highlighted.scrollTop += (er.top - hr.top) - highlighted.clientHeight / 3;
633
+ el.classList.remove('flash');
634
+ void el.offsetWidth;
635
+ el.classList.add('flash');
636
+ }
637
+
638
+ // 1-я колонка: по доле текста (приблизительно, но рядом с нужным местом)
639
+ const g = ch.start + m.offset;
640
+ const max = input.scrollHeight - input.clientHeight;
641
+ if (max > 0 && fullText.length) {
642
+ input.scrollTop = Math.max(0, (g / fullText.length) * input.scrollHeight - input.clientHeight / 3);
643
+ }
644
+
645
+ requestAnimationFrame(() => requestAnimationFrame(() => { syncing = false; }));
646
+ }
647
+
648
  /* ---------- Прогон всех кусков ---------- */
649
  document.getElementById('btnRunAll').onclick = async () => {
650
  if (running) return;