feat(trainer): ИИ-репетитор — разбор ошибок и наводящие подсказки (направление A)
Безопасно через grounding: модели ДАЮТСЯ задача, правильный ответ и шаги (вычислены движком детерминированно) — ИИ только ОБЪЯСНЯЕТ, не считает. Поэтому даже слабая модель не выдаст неверную математику.
- сервис practiceExplainService.explain({problem, studentAnswer, mode, ask}): mode 'mistake' (разбор ошибки, можно назвать ответ) / 'hint' (наводящая подсказка БЕЗ ответа). Текст модели чистится от markdown и экранируется; LLM-вызов инъектируется (тесты), реальный — callLLMFailover (провайдеры Квантик-ассистента)
- POST /api/practice/explain (auth-only, ученикам); нет/выключен LLM → 503, клиент мягко падает на пошаговое решение
- клиент LS.practiceExplain; на странице кнопка «Объяснить»: после неверного ответа → разбор (с ответом ученика), иначе → подсказка; рендер в .tr-ai-box (текст экранирован сервером)
- тест practice-explain 7/7 (grounding: ответ в промпте; hint не раскрывает ответ; off→not ok; cleanText экранирует; endpoint 503/400/401)
- бэкенд practice-тесты 40/40, страница 42/42, lint:routes 0 unprotected, эмодзи 0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+35
-2
@@ -264,6 +264,14 @@
|
||||
.tr-feedback.warn { color: var(--warn); background: #fef3c7; font-weight: 700; }
|
||||
|
||||
.tr-actions { display: flex; flex-wrap: wrap; gap: 9px; justify-content: center; margin-top: 18px; }
|
||||
/* ИИ-репетитор */
|
||||
.tr-ai-btn { color: var(--accent-ink); background: rgba(99,102,241,.1); border: 1px solid rgba(99,102,241,.28); }
|
||||
.tr-ai-btn:hover { background: rgba(99,102,241,.18); }
|
||||
.tr-ai-btn .ic { color: var(--g1); }
|
||||
.tr-ai-box { margin-top: 18px; padding: 16px 18px; border-radius: var(--r-md); background: linear-gradient(180deg, #f6f5ff, #eef0ff); border: 1px solid rgba(99,102,241,.22); animation: trUp .3s var(--ease) both; }
|
||||
.tr-ai-hd { display: flex; align-items: center; gap: 7px; font-size: .72rem; font-weight: 800; text-transform: uppercase; letter-spacing: .07em; color: var(--accent-ink); margin-bottom: 9px; }
|
||||
.tr-ai-hd .ic { width: 15px; height: 15px; color: var(--g1); }
|
||||
.tr-ai-body { color: #334155; font-size: .98rem; line-height: 1.6; }
|
||||
|
||||
/* пошаговое решение / репетитор (P7) */
|
||||
.tr-steps { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
|
||||
@@ -470,6 +478,10 @@
|
||||
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.1h6c0-.8.4-1.6 1-2.1A7 7 0 0 0 12 2Z"/></svg>
|
||||
Подсказка
|
||||
</button>
|
||||
<button class="tr-btn tr-ai-btn" id="tr-ai" type="button" title="ИИ-репетитор объяснит">
|
||||
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.6L18.5 9l-4.7 1.4L12 15l-1.8-4.6L5.5 9l4.7-1.4z"/><path d="M19 14l.7 1.8L21.5 16.5l-1.8.7L19 19l-.7-1.8L16.5 16.5l1.8-.7z"/></svg>
|
||||
Объяснить
|
||||
</button>
|
||||
<button class="tr-btn tr-ghost" id="tr-solve" type="button">
|
||||
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
||||
Решение
|
||||
@@ -480,6 +492,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tr-ai-box" id="tr-ai-box" style="display:none"></div>
|
||||
<div class="tr-solution" id="tr-solution" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -650,7 +663,7 @@
|
||||
var curTopic = topics[0] ? topics[0].key : null;
|
||||
var curGen = skillsOf(curTopic)[0] || gens[0];
|
||||
var cur = null;
|
||||
var solved = 0, streak = 0;
|
||||
var solved = 0, streak = 0, lastWrong = false;
|
||||
var answered = false; // задача решена (верно/неверно/показано решение) → «Проверить» становится «Дальше»
|
||||
var prog = {}; // skill → строка прогресса с сервера
|
||||
|
||||
@@ -921,6 +934,8 @@
|
||||
inp.value = ''; inp.disabled = false;
|
||||
var fb = $('tr-feedback'); fb.className = 'tr-feedback'; fb.textContent = '';
|
||||
$('tr-solution').style.display = 'none'; $('tr-solution').innerHTML = '';
|
||||
var aiBox = $('tr-ai-box'); if (aiBox) { aiBox.style.display = 'none'; aiBox.innerHTML = ''; }
|
||||
lastWrong = false;
|
||||
var card = $('tr-card'); if (card) { card.classList.remove('tr-correct'); card.classList.remove('tr-wrong'); }
|
||||
var pv = $('tr-preview'); if (pv) pv.innerHTML = '';
|
||||
setMode(false);
|
||||
@@ -1030,7 +1045,7 @@
|
||||
$('tr-input').disabled = true;
|
||||
onSolved();
|
||||
} else {
|
||||
streak = 0;
|
||||
streak = 0; lastWrong = true;
|
||||
// репетитор (C1): адресная подсказка по типовой ошибке, не выдавая ответ
|
||||
var diag = TE.analyzeMistake ? TE.analyzeMistake(cur, r.value) : null;
|
||||
var msg = diag ? diag.hint : 'Неверно. Разбери решение и реши похожую.';
|
||||
@@ -1229,6 +1244,24 @@
|
||||
});
|
||||
$('tr-check').addEventListener('click', check);
|
||||
$('tr-skip').addEventListener('click', newProblem);
|
||||
// ИИ-репетитор: после неверного ответа — разбор ошибки; иначе — наводящая подсказка.
|
||||
// Опирается на известный ответ + шаги (движок), ИИ только ОБЪЯСНЯЕТ. Недоступен ИИ → решение.
|
||||
function aiExplain() {
|
||||
if (!cur || !LS.practiceExplain) return;
|
||||
var box = $('tr-ai-box'); if (!box) return;
|
||||
var mode = lastWrong ? 'mistake' : 'hint';
|
||||
var sparkIc = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.6L18.5 9l-4.7 1.4L12 15l-1.8-4.6L5.5 9l4.7-1.4z"/></svg>';
|
||||
box.style.display = 'block';
|
||||
box.innerHTML = '<div class="tr-ai-hd">' + sparkIc + ' Квантик ' + (mode === 'mistake' ? 'разбирает ошибку' : 'подсказывает') + '</div><div class="tr-ai-body" id="tr-ai-body">Думаю…</div>';
|
||||
var bodyEl = $('tr-ai-body');
|
||||
var steps = (cur.solution || []).map(function (s) { return { note: s.note || '', tex: s.tex || '' }; });
|
||||
var payload = { display: cur.display || answerLabel(), answer: answerLabel(), steps: steps, studentAnswer: ($('tr-input').value || ''), mode: mode };
|
||||
LS.practiceExplain(payload).then(function (r) {
|
||||
if (r && r.ok && r.text) bodyEl.innerHTML = String(r.text).replace(/\n/g, '<br>'); // текст уже экранирован сервером
|
||||
else { bodyEl.textContent = 'ИИ-репетитор сейчас недоступен — посмотри пошаговое решение ниже.'; revealSolution(); }
|
||||
}).catch(function () { bodyEl.textContent = 'ИИ-репетитор недоступен — смотри решение ниже.'; revealSolution(); });
|
||||
}
|
||||
$('tr-ai').addEventListener('click', aiExplain);
|
||||
$('tr-hint').addEventListener('click', function () {
|
||||
if (!cur) return;
|
||||
if (stepMode) {
|
||||
|
||||
Reference in New Issue
Block a user