From 99a00e074348ac9855767f380789a15d82d9cd85 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Sat, 13 Jun 2026 14:48:02 +0300 Subject: [PATCH] Training works great --- src/algorithm.ts | 32 ++++++++++------ src/db-manager.ts | 20 +++++++++- src/main.ts | 2 + src/training.ts | 97 +++++++++++++++++++++++------------------------ 4 files changed, 88 insertions(+), 63 deletions(-) diff --git a/src/algorithm.ts b/src/algorithm.ts index f0658e3..95b2055 100644 --- a/src/algorithm.ts +++ b/src/algorithm.ts @@ -56,7 +56,8 @@ export function calculatedScore(stat: CardStatistics): number { const diffMs = now - stat.last_open; const days = diffMs / (1000 * 60 * 60 * 24); const multiplier = Math.pow(FADE_PER_DAY, Math.max(0, days)); - return stat.score * multiplier; + const v = stat.score * multiplier; + return v; } export function updateStatScore(stat: CardStatistics, status: WordOpenMode): void { @@ -154,17 +155,25 @@ class SemiRandomSRSModule implements SRSModule { private initialized = false; next(set: { words: WordData[]; set: CardStatistics[] }): number { + const maxHistory = this.historyLen(set.set.length); + + // Пробуем выбрать индекс не из истории (не более 100 попыток) + for (let attempt = 0; attempt < 100; attempt++) { + const index = this.lastWeights.sample(); + if (!this.history.includes(index) || maxHistory >= set.set.length) { + // Если история может вместить все слова — пропускаем проверку + if (this.history.length >= maxHistory) { + this.history.shift(); + } + this.history.push(index); + return index; + } + } + + // Fallback: если все индексы в истории, очищаем историю + this.history = []; const index = this.lastWeights.sample(); - - if (this.history.includes(index)) { - return this.next(set); - } - - if (this.history.length >= this.historyLen(set.set.length)) { - this.history.shift(); - } this.history.push(index); - return index; } @@ -178,11 +187,12 @@ class SemiRandomSRSModule implements SRSModule { const weights = set.set.map( (s) => Math.pow(100.0 / Math.max(0.1, calculatedScore(s)), 2.0) * 2.0, ); + this.lastWeights = new WeightedIndex(weights); } private historyLen(setLen: number): number { - return Math.min(MAX_HISTORY_LEN, Math.floor(setLen * MAX_HISTORY_LEN_PART)); + return Math.min(MAX_HISTORY_LEN, Math.max(0, Math.floor(setLen * MAX_HISTORY_LEN_PART))); } } diff --git a/src/db-manager.ts b/src/db-manager.ts index cbc9e6a..c404429 100644 --- a/src/db-manager.ts +++ b/src/db-manager.ts @@ -25,6 +25,20 @@ export interface CardStatRecord { last_opened: number; } +/** + * Парсит last_opened из SQLite. + * SQLite может вернуть ISO-строку "2026-05-22 10:52:06.823259+00:00", + * хотя в схеме поле INTEGER. Приводим к unix timestamp (секунды). + */ +function parseLastOpened(value: any): number { + if (typeof value === 'number') return value; + if (typeof value === 'string') { + const ts = Date.parse(value); + if (!isNaN(ts)) return Math.floor(ts / 1000); + } + return Math.floor(Date.now() / 1000); +} + /** * Получает все колоды (card_set). */ @@ -101,7 +115,7 @@ export async function getWordsAndStatsForSet( word_id: r[1], set_id: r[2], score: r[3], - last_opened: r[4], + last_opened: parseLastOpened(r[4]), })); return { words, stats }; @@ -115,8 +129,10 @@ export async function updateCardStat( score: number, lastOpened: number, ): Promise { + // Убеждаемся, что score — число >= 1 + const safeScore = Math.max(1, Math.round(score || 1)); await executeVoid( `UPDATE card_stats SET score = ?, last_opened = ? WHERE id = ?`, - [score, lastOpened, statId], + [safeScore, lastOpened, statId], ); } diff --git a/src/main.ts b/src/main.ts index 2012a5a..d4a3eea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -193,6 +193,8 @@ async function startTraining(setId: number) { return; } + console.log(`[Training] Запуск тренировки: ${words.length} слов, ${stats.length} записей статистики, режим: ${mode}`); + const settings = { id: set.id, name: set.name, diff --git a/src/training.ts b/src/training.ts index 1b0dfd7..e5a6059 100644 --- a/src/training.ts +++ b/src/training.ts @@ -11,11 +11,28 @@ export interface WordItem { group_id: number; } -function getFieldValues(more: Record, fields: string): string[] { +/** + * Извлекает значения полей из слова. + * Сначала ищет в основных полях (key, value, tags), + * потом в more (JSON-словарь). + * Если поле не найдено — пропускается (возвращается пустая строка, фильтруется). + */ +function getWordFieldValues(word: WordItem, fields: string): string[] { + const directFields: Record = { + key: word.key, + value: word.value, + tags: word.tags, + }; + return fields .split(/\s+/) .filter(Boolean) - .map((f) => more[f] || f); + .map((f) => { + if (f in directFields && directFields[f]) return directFields[f]; + if (f in word.more && word.more[f]) return word.more[f]; + return ''; + }) + .filter(Boolean); } export interface TrainingCallbacks { @@ -28,6 +45,8 @@ export class TrainingPage { private words: WordItem[]; private callbacks: TrainingCallbacks; private totalCards: number; + private forwardFields: string; + private backwardFields: string; constructor( container: HTMLElement, @@ -39,6 +58,8 @@ export class TrainingPage { this.container = container; this.callbacks = callbacks; this.totalCards = stats.length; + this.forwardFields = settings.forward || 'key'; + this.backwardFields = settings.backward || 'value'; const wordData: WordData[] = words.map((w) => ({ id: w.id, @@ -55,13 +76,12 @@ export class TrainingPage { } private render(): void { - const current = this.cardSet.currentWordIndex; const total = this.totalCards; this.container.innerHTML = `
- ${current !== null ? current + 1 : 0} / ${total} + слов: ${total}
@@ -83,11 +103,7 @@ export class TrainingPage { const word = this.words.find((w) => w.id === result.word.id); if (!word) return; - const settings = (this.cardSet as any).settings as CardSetSettings | undefined; - const forwardFields = settings?.forward || 'key'; - const backwardFields = settings?.backward || 'value'; - - this.renderSide(word, forwardFields, backwardFields, false); + this.renderSide(word, this.forwardFields, this.backwardFields, false); this.renderActions(false); } @@ -97,27 +113,33 @@ export class TrainingPage { backwardFields: string, showBack: boolean, ): void { - const more = word.more; - const contentEl = document.getElementById('training-content'); if (!contentEl) return; if (!showBack) { - const values = getFieldValues(more, forwardFields); + const values = getWordFieldValues(word, forwardFields); + if (values.length === 0) { + contentEl.innerHTML = '
Нет данных для отображения
'; + return; + } contentEl.innerHTML = values .map((v) => `
${this.escapeHtml(v)}
`) .join(''); } else { - const fwdVals = getFieldValues(more, forwardFields); - const bwdVals = getFieldValues(more, backwardFields); + const fwdVals = getWordFieldValues(word, forwardFields); + const bwdVals = getWordFieldValues(word, backwardFields); contentEl.innerHTML = `
- ${fwdVals.map((v) => `
${this.escapeHtml(v)}
`).join('')} + ${fwdVals.length > 0 + ? fwdVals.map((v) => `
${this.escapeHtml(v)}
`).join('') + : '
'}
- ${bwdVals.map((v) => `
${this.escapeHtml(v)}
`).join('')} + ${bwdVals.length > 0 + ? bwdVals.map((v) => `
${this.escapeHtml(v)}
`).join('') + : '
'}
`; } @@ -157,11 +179,7 @@ export class TrainingPage { const word = this.words[current]; if (!word) return; - const settings = (this.cardSet as any).settings as CardSetSettings | undefined; - const forwardFields = settings?.forward || 'key'; - const backwardFields = settings?.backward || 'value'; - - this.renderSide(word, forwardFields, backwardFields, true); + this.renderSide(word, this.forwardFields, this.backwardFields, true); this.renderActions(true); } @@ -172,37 +190,16 @@ export class TrainingPage { if (current === null) return; const stat = this.cardSet.set[current]; - try { - await updateCardStat(stat.id, stat.score, Math.floor(stat.last_open / 1000)); - } catch (err) { - console.error('[Training] Ошибка сохранения:', err); + if (stat) { + try { + await updateCardStat(stat.id, stat.score, Math.floor(stat.last_open / 1000)); + } catch (err) { + console.error('[Training] Ошибка сохранения:', err); + } } - if (current < this.totalCards - 1) { - this.renderCard(); - } else { - this.finish(); - } - } - - private finish(): void { - this.container.innerHTML = ` -
- -
-
-
-
🎉
-

Тренировка завершена!

-

Все ${this.totalCards} карточек пройдены.

-
-
-
- `; - - document.getElementById('btnTrainingFinish')?.addEventListener('click', () => { - this.callbacks.onFinish(); - }); + // Бесконечная тренировка — всегда переходим к следующей карте + this.renderCard(); } private escapeHtml(text: string): string {