feat(biochem): Фаза 5.2 — живая поэлементная проверка баланса в задании

В balance-задании по мере ввода коэффициентов показывается счётчик атомов
каждого элемента слева=справа с ✓/✗ и бейджем «сбалансировано» (через
BIO.parseFormula). Обучающая обратная связь до отправки ответа.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 15:12:25 +03:00
parent a9cf8c049d
commit b9d30f5252
+35 -3
View File
@@ -1925,21 +1925,53 @@ function renderBalanceChallenge(data) {
wrap.style.display = '';
const reactants = data.reactants || [];
const products = data.products || [];
const count = reactants.length + products.length;
wrap._reactants = reactants;
wrap._products = products;
let html = '<div class="balance-eq">';
reactants.forEach((f,i) => {
if (i > 0) html += '<span class="bal-op">+</span>';
html += `<input type="number" class="bal-coef" min="1" max="99" value="1" placeholder="?">`;
html += `<input type="number" class="bal-coef" min="1" max="99" value="1" placeholder="?" oninput="updateBalanceFeedback()">`;
html += `<span class="bal-formula">${escHtml(f)}</span>`;
});
html += '<span class="bal-arrow"><svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></span>';
products.forEach((f,i) => {
if (i > 0) html += '<span class="bal-op">+</span>';
html += `<input type="number" class="bal-coef" min="1" max="99" value="1" placeholder="?">`;
html += `<input type="number" class="bal-coef" min="1" max="99" value="1" placeholder="?" oninput="updateBalanceFeedback()">`;
html += `<span class="bal-formula">${escHtml(f)}</span>`;
});
html += '</div>';
// живая поэлементная проверка сохранения атомов
html += '<div id="bal-feedback" style="display:flex;flex-wrap:wrap;gap:5px;margin-top:2px"></div>';
wrap.innerHTML = html;
updateBalanceFeedback();
}
// Живой счётчик атомов слева/справа по каждому элементу (через BIO.parseFormula)
function updateBalanceFeedback() {
const wrap = document.getElementById('bp-chal-balance');
const fb = document.getElementById('bal-feedback');
if (!wrap || !fb || !window.BIO) return;
const reactants = wrap._reactants || [], products = wrap._products || [];
const coefs = Array.from(wrap.querySelectorAll('.bal-coef')).map(i => parseInt(i.value) || 0);
const sideCounts = (formulas, offset) => {
const tot = {};
formulas.forEach((f, i) => {
const k = coefs[offset + i] || 0;
const c = BIO.parseFormula(f);
for (const el in c) tot[el] = (tot[el] || 0) + c[el] * k;
});
return tot;
};
const L = sideCounts(reactants, 0);
const R = sideCounts(products, reactants.length);
const els = [...new Set([...Object.keys(L), ...Object.keys(R)])].sort();
let allOk = els.length > 0;
fb.innerHTML = els.map(el => {
const l = L[el] || 0, r = R[el] || 0, ok = l === r;
if (!ok) allOk = false;
const col = ok ? '#4ade80' : '#f87171';
return `<span style="font-size:.66rem;font-weight:700;padding:2px 7px;border-radius:999px;border:1px solid ${col}40;color:${col}">${el}: ${l}=${r} ${ok ? '✓' : '✗'}</span>`;
}).join('') + (allOk ? '<span style="font-size:.66rem;font-weight:700;padding:2px 7px;border-radius:999px;background:rgba(74,222,128,.14);color:#4ade80">сбалансировано</span>' : '');
}
// Keyboard shortcuts