feat(flashcards): Anki-стиль интервалов — кнопки различаются
Раньше на новой карте Снова/Трудно/Знаю/Легко все давали 1 день (чистый SM-2: оценка влияла только на ease factor). Теперь интервал зависит от оценки: новая карта Легко=4д (остальные 1д), на повторах Трудно ×1.2 / Знаю ×ef / Легко ×ef×1.3 (easy-бонус). Серверный sm2() и клиентское превью fcNextInterval синхронны — проверено 0 расхождений на 256 комбинациях. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,14 @@ function safeImg(url) {
|
|||||||
return /^\/uploads\/flashcards\/[A-Za-z0-9._-]+$/.test(u) ? u : '';
|
return /^\/uploads\/flashcards\/[A-Za-z0-9._-]+$/.test(u) ? u : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── SM-2 algorithm ───────────────────────────────────────────────────────
|
/* ── SM-2 (Anki-стиль: кнопки различаются) ─────────────────────────────────
|
||||||
quality: 0 = blackout, 1 = wrong, 2 = wrong but familiar,
|
quality: 0/2 = Снова, 3 = Трудно, 4 = Знаю, 5 = Легко.
|
||||||
3 = correct with difficulty, 4 = correct, 5 = perfect
|
В отличие от чистого SM-2, интервал зависит от оценки уже на первых повторах:
|
||||||
|
на новой карте Снова/Трудно/Знаю → 1д, Легко → 4д; на зрелых — Трудно ×1.2,
|
||||||
|
Знаю ×ef, Легко ×ef×1.3 (easy-бонус). ВАЖНО: клиентское превью
|
||||||
|
fcNextInterval() в flashcards.html — точная копия этой логики интервалов.
|
||||||
─────────────────────────────────────────────────────────────────────── */
|
─────────────────────────────────────────────────────────────────────── */
|
||||||
|
const FC_HARD_MULT = 1.2, FC_EASY_BONUS = 1.3;
|
||||||
function sm2(easeFactor, intervalDays, repetitions, quality) {
|
function sm2(easeFactor, intervalDays, repetitions, quality) {
|
||||||
let ef = easeFactor;
|
let ef = easeFactor;
|
||||||
let n = repetitions;
|
let n = repetitions;
|
||||||
@@ -21,11 +25,17 @@ function sm2(easeFactor, intervalDays, repetitions, quality) {
|
|||||||
|
|
||||||
if (quality < 3) {
|
if (quality < 3) {
|
||||||
n = 0;
|
n = 0;
|
||||||
iv = 1;
|
iv = 1; // Снова — сброс
|
||||||
} else {
|
} else {
|
||||||
if (n === 0) iv = 1;
|
if (n === 0) {
|
||||||
else if (n === 1) iv = 6;
|
iv = (quality === 5) ? 4 : 1; // выпуск: Легко 4д, иначе 1д
|
||||||
else iv = Math.round(iv * ef);
|
} else if (n === 1) {
|
||||||
|
iv = (quality === 3) ? 3 : (quality === 4) ? 6 : Math.round(6 * FC_EASY_BONUS);
|
||||||
|
} else {
|
||||||
|
if (quality === 3) iv = Math.max(iv + 1, Math.round(iv * FC_HARD_MULT));
|
||||||
|
else if (quality === 4) iv = Math.round(iv * ef);
|
||||||
|
else iv = Math.round(iv * ef * FC_EASY_BONUS);
|
||||||
|
}
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
ef = Math.max(1.3, ef + 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
|
ef = Math.max(1.3, ef + 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
|
||||||
|
|||||||
@@ -1569,20 +1569,21 @@ function finishStudy() {
|
|||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── estimated next interval preview for sq buttons ──
|
/* ── превью следующего интервала для кнопок Снова/Трудно/Знаю/Легко ──
|
||||||
ВАЖНО: точная копия серверного sm2() (flashcardController.js), иначе
|
ВАЖНО: точная копия логики интервалов серверного sm2() (flashcardController.js),
|
||||||
превью врёт. В чистом SM-2 интервал для q>=3 НЕ зависит от значения q
|
иначе превью врёт. Anki-стиль: на новой карте Легко=4д выделяется, на зрелых
|
||||||
(q влияет только на ease factor), поэтому «Трудно/Знаю/Легко» при первых
|
Трудно ×1.2 / Знаю ×ef / Легко ×ef×1.3. */
|
||||||
повторениях дают одинаковый интервал — это корректно.
|
const FC_HARD_MULT = 1.2, FC_EASY_BONUS = 1.3;
|
||||||
(Дифференциация интервалов по кнопкам — кандидат на Фазу 4.) */
|
|
||||||
function fcNextInterval(card, q) {
|
function fcNextInterval(card, q) {
|
||||||
const ef = card.ease_factor || 2.5;
|
const ef = card.ease_factor || 2.5;
|
||||||
const iv = card.interval_days || 1;
|
const iv = card.interval_days || 1;
|
||||||
const rep = card.repetitions || 0;
|
const rep = card.repetitions || 0;
|
||||||
if (q < 3) return 1;
|
if (q < 3) return 1;
|
||||||
if (rep === 0) return 1;
|
if (rep === 0) return q === 5 ? 4 : 1;
|
||||||
if (rep === 1) return 6;
|
if (rep === 1) return q === 3 ? 3 : q === 4 ? 6 : Math.round(6 * FC_EASY_BONUS);
|
||||||
return Math.round(iv * ef);
|
if (q === 3) return Math.max(iv + 1, Math.round(iv * FC_HARD_MULT));
|
||||||
|
if (q === 4) return Math.round(iv * ef);
|
||||||
|
return Math.round(iv * ef * FC_EASY_BONUS);
|
||||||
}
|
}
|
||||||
function fcDaysLabel(n) {
|
function fcDaysLabel(n) {
|
||||||
if (n <= 1) return '1 день';
|
if (n <= 1) return '1 день';
|
||||||
|
|||||||
Reference in New Issue
Block a user