feat(vocab): update intensive review UI and logic for better user guidance

This commit is contained in:
Torsten Schulz (local)
2026-08-26 10:16:41 +02:00
parent bac915c328
commit 68470bf576
7 changed files with 42 additions and 43 deletions

View File

@@ -2022,15 +2022,12 @@ export default class VocabService {
const requestedLimit = this._clampInteger(query?.limit, { min: 1, max: DAILY_SRS_LIMIT, fallback: DAILY_SRS_LIMIT }); const requestedLimit = this._clampInteger(query?.limit, { min: 1, max: DAILY_SRS_LIMIT, fallback: DAILY_SRS_LIMIT });
const limit = Math.min(DAILY_SRS_LIMIT, requestedLimit); const limit = Math.min(DAILY_SRS_LIMIT, requestedLimit);
const now = new Date(); const now = new Date();
const dueWhere = { // SRS is a daily review: a term scheduled for any time today belongs to
userId: user.id, // today's batch. Older rows remain due as well. Comparing calendar dates
courseId: Number(course.id), // avoids an accidental time-of-day gate for a daily task.
nextDueAt: { const todayKey = this._getSrsScheduleDateKey(now);
[Op.lte]: now const scheduledRows = await VocabSrsItem.findAll({
} where: { userId: user.id, courseId: Number(course.id) },
};
const dueRows = await VocabSrsItem.findAll({
where: dueWhere,
order: [ order: [
['nextDueAt', 'ASC'], ['nextDueAt', 'ASC'],
['wrongCount', 'DESC'], ['wrongCount', 'DESC'],
@@ -2043,20 +2040,17 @@ export default class VocabService {
.map((entry) => String(entry?.itemKey || '').trim()) .map((entry) => String(entry?.itemKey || '').trim())
.filter(Boolean) .filter(Boolean)
); );
const validDueRows = dueRows.filter((item) => const validDueRows = scheduledRows.filter((item) =>
this._isTrainableSrsPair(item) this._isTrainableSrsPair(item)
&& (!validKeys.size || validKeys.has(String(item.itemKey || '').trim())) && (!validKeys.size || validKeys.has(String(item.itemKey || '').trim()))
&& this._getSrsScheduleDateKey(item.nextDueAt) <= todayKey
); );
const scheduledRows = await VocabSrsItem.findAll({
where: { userId: user.id, courseId: Number(course.id) },
order: [['nextDueAt', 'ASC']]
});
const upcomingByDate = new Map(); const upcomingByDate = new Map();
scheduledRows scheduledRows
.filter((item) => ( .filter((item) => (
this._isTrainableSrsPair(item) this._isTrainableSrsPair(item)
&& (!validKeys.size || validKeys.has(String(item.itemKey || '').trim())) && (!validKeys.size || validKeys.has(String(item.itemKey || '').trim()))
&& new Date(item.nextDueAt).getTime() > now.getTime() && this._getSrsScheduleDateKey(item.nextDueAt) > todayKey
)) ))
.forEach((item) => { .forEach((item) => {
const date = this._getSrsScheduleDateKey(item.nextDueAt); const date = this._getSrsScheduleDateKey(item.nextDueAt);

View File

@@ -441,9 +441,14 @@
"courseFlowBlockTitle": "Kasamtangang block", "courseFlowBlockTitle": "Kasamtangang block",
"courseFlowBlockDescription": "Dinhi nahimutang ang sunod regular nga lakang sa kurso.", "courseFlowBlockDescription": "Dinhi nahimutang ang sunod regular nga lakang sa kurso.",
"courseFlowBlockEmpty": "Nahuman na ang kasamtangang block o walay abli nga leksiyon niini karon.", "courseFlowBlockEmpty": "Nahuman na ang kasamtangang block o walay abli nga leksiyon niini karon.",
"courseFlowIntensiveTitle": "Angay nga intensive phase", "courseFlowIntensiveTitle": "Sunod nga adlaw-adlaw nga balik-balik",
"courseFlowIntensiveDescription": "Mas dikit nga balik-balik sa dihang lig-on na ang nag-una nga block.", "courseFlowIntensiveDescription": "Ang petsa sa balik-balik sa imong mga termino nagsalig sa imong miaging sakto nga tubag.",
"courseFlowIntensiveEmpty": "Wala pay bag-ong intensive phase nga naablihan karon.", "courseFlowIntensiveEmpty": "Matan-aw nimo dinhi ang sunod nga mga petsa.",
"courseFlowIntensiveStatusAction": "Tan-awa ang sunod nga balik-balik",
"courseFlowIntensiveStatusTitle": "Sunod nga balik-balik sa bokabularyo",
"courseFlowIntensiveStatusAllDone": "Wala pay dugang nga balik-balik sa bokabularyo nga nakaplano alang sa nahuman nga mga leksiyon.",
"courseFlowIntensiveStatusProgress": "{count} ka termino",
"courseFlowIntensiveStatusNoDate": "Ang mga petsa nagsalig sa imong sakto nga tubag: 1, 3, 7, 14, 30, 60 ug 120 ka adlaw. Ang lisod nga mga termino gilain ug praktisahon sa lugar sa lisod nga mga termino.",
"courseFlowPracticeTitle": "Libre nga pagpalalom", "courseFlowPracticeTitle": "Libre nga pagpalalom",
"courseFlowPracticeDescription": "Nahuman nang mga leksiyon para sa hinay-hinay nga dugang praktis gawas sa obligadong agi-anan.", "courseFlowPracticeDescription": "Nahuman nang mga leksiyon para sa hinay-hinay nga dugang praktis gawas sa obligadong agi-anan.",
"courseFlowPracticeEmpty": "Sa dihang makahuman ka sa unang mga leksiyon, makita sila dinhi para sa libre nga praktis.", "courseFlowPracticeEmpty": "Sa dihang makahuman ka sa unang mga leksiyon, makita sila dinhi para sa libre nga praktis.",

View File

@@ -771,14 +771,14 @@
"courseFlowBlockTitle": "Aktueller Block", "courseFlowBlockTitle": "Aktueller Block",
"courseFlowBlockDescription": "Hier liegt der nächste reguläre Fortschritt im Kurs.", "courseFlowBlockDescription": "Hier liegt der nächste reguläre Fortschritt im Kurs.",
"courseFlowBlockEmpty": "Der aktuelle Block ist bereits abgeschlossen oder es gibt gerade keine offene Blocklektion.", "courseFlowBlockEmpty": "Der aktuelle Block ist bereits abgeschlossen oder es gibt gerade keine offene Blocklektion.",
"courseFlowIntensiveTitle": "Fällige Intensivphase", "courseFlowIntensiveTitle": "Nächste Tageswiederholungen",
"courseFlowIntensiveDescription": "Zusätzliche Wiederholung für Begriffe, die du in der Tageswiederholung falsch beantwortet hast.", "courseFlowIntensiveDescription": "Die Wiederholungstermine deiner Begriffe richten sich nach den bisherigen richtigen Antworten.",
"courseFlowIntensiveEmpty": "Aktuell sind keine Begriffe als schwer markiert.", "courseFlowIntensiveEmpty": "Die nächsten Termine kannst du hier einsehen.",
"courseFlowIntensiveStatusAction": "Nächste Wiederholungen prüfen", "courseFlowIntensiveStatusAction": "Nächste Wiederholungen prüfen",
"courseFlowIntensiveStatusTitle": "Nächste Vokabelwiederholungen", "courseFlowIntensiveStatusTitle": "Nächste Vokabelwiederholungen",
"courseFlowIntensiveStatusAllDone": "Für abgeschlossene Lektionen sind derzeit keine weiteren Vokabelwiederholungen geplant.", "courseFlowIntensiveStatusAllDone": "Für abgeschlossene Lektionen sind derzeit keine weiteren Vokabelwiederholungen geplant.",
"courseFlowIntensiveStatusProgress": "{count} Begriffe", "courseFlowIntensiveStatusProgress": "{count} Begriffe",
"courseFlowIntensiveStatusNoDate": "Die Termine richten sich nach deinen richtigen Antworten: 1, 3, 7, 14, 30, 60 und 120 Tage. Eine falsche Antwort wird als schwer markiert und kommt zeitnah wieder.", "courseFlowIntensiveStatusNoDate": "Die Termine richten sich nach deinen richtigen Antworten: 1, 3, 7, 14, 30, 60 und 120 Tage. Schwer markierte Begriffe werden getrennt im Bereich „Schwere Begriffe“ geübt.",
"courseFlowPracticeTitle": "Freie Vertiefung", "courseFlowPracticeTitle": "Freie Vertiefung",
"courseFlowPracticeDescription": "Abgeschlossene Lektionen für lockeres Nachtrainieren außerhalb des Pflichtpfads.", "courseFlowPracticeDescription": "Abgeschlossene Lektionen für lockeres Nachtrainieren außerhalb des Pflichtpfads.",
"courseFlowPracticeEmpty": "Sobald du erste Lektionen abgeschlossen hast, erscheinen sie hier für freies Nachtrainieren.", "courseFlowPracticeEmpty": "Sobald du erste Lektionen abgeschlossen hast, erscheinen sie hier für freies Nachtrainieren.",

View File

@@ -771,14 +771,14 @@
"courseFlowBlockTitle": "Current block", "courseFlowBlockTitle": "Current block",
"courseFlowBlockDescription": "This is where the next regular progress step in the course lives.", "courseFlowBlockDescription": "This is where the next regular progress step in the course lives.",
"courseFlowBlockEmpty": "The current block is already done or there is no open block lesson right now.", "courseFlowBlockEmpty": "The current block is already done or there is no open block lesson right now.",
"courseFlowIntensiveTitle": "Due intensive review", "courseFlowIntensiveTitle": "Upcoming daily reviews",
"courseFlowIntensiveDescription": "Extra review for terms answered incorrectly in the daily review.", "courseFlowIntensiveDescription": "Each term's review date follows your previous correct answers.",
"courseFlowIntensiveEmpty": "No terms are currently marked as hard.", "courseFlowIntensiveEmpty": "You can view the upcoming dates here.",
"courseFlowIntensiveStatusAction": "Check upcoming reviews", "courseFlowIntensiveStatusAction": "Check upcoming reviews",
"courseFlowIntensiveStatusTitle": "Upcoming vocabulary reviews", "courseFlowIntensiveStatusTitle": "Upcoming vocabulary reviews",
"courseFlowIntensiveStatusAllDone": "No further vocabulary reviews are currently scheduled for completed lessons.", "courseFlowIntensiveStatusAllDone": "No further vocabulary reviews are currently scheduled for completed lessons.",
"courseFlowIntensiveStatusProgress": "{count} terms", "courseFlowIntensiveStatusProgress": "{count} terms",
"courseFlowIntensiveStatusNoDate": "Dates are based on correct answers: 1, 3, 7, 14, 30, 60 and 120 days. An incorrect answer is marked hard and returns soon.", "courseFlowIntensiveStatusNoDate": "Dates are based on correct answers: 1, 3, 7, 14, 30, 60 and 120 days. Terms marked hard are practised separately in the hard-terms area.",
"courseFlowPracticeTitle": "Free practice", "courseFlowPracticeTitle": "Free practice",
"courseFlowPracticeDescription": "Completed lessons for relaxed extra practice outside the required path.", "courseFlowPracticeDescription": "Completed lessons for relaxed extra practice outside the required path.",
"courseFlowPracticeEmpty": "As soon as you complete your first lessons, they will appear here for free practice.", "courseFlowPracticeEmpty": "As soon as you complete your first lessons, they will appear here for free practice.",

View File

@@ -750,9 +750,14 @@
"courseFlowBlockTitle": "Bloque actual", "courseFlowBlockTitle": "Bloque actual",
"courseFlowBlockDescription": "Aquí está el siguiente progreso regular dentro del curso.", "courseFlowBlockDescription": "Aquí está el siguiente progreso regular dentro del curso.",
"courseFlowBlockEmpty": "El bloque actual ya está terminado o ahora mismo no hay ninguna lección abierta del bloque.", "courseFlowBlockEmpty": "El bloque actual ya está terminado o ahora mismo no hay ninguna lección abierta del bloque.",
"courseFlowIntensiveTitle": "Fase intensiva pendiente", "courseFlowIntensiveTitle": "Próximos repasos diarios",
"courseFlowIntensiveDescription": "Repaso concentrado cuando el bloque anterior ya está bastante asentado.", "courseFlowIntensiveDescription": "Las fechas de repaso de tus términos dependen de tus respuestas correctas anteriores.",
"courseFlowIntensiveEmpty": "Ahora mismo no hay ninguna nueva fase intensiva desbloqueada.", "courseFlowIntensiveEmpty": "Puedes consultar aquí las próximas fechas.",
"courseFlowIntensiveStatusAction": "Consultar próximos repasos",
"courseFlowIntensiveStatusTitle": "Próximos repasos de vocabulario",
"courseFlowIntensiveStatusAllDone": "Actualmente no hay más repasos de vocabulario programados para las lecciones completadas.",
"courseFlowIntensiveStatusProgress": "{count} términos",
"courseFlowIntensiveStatusNoDate": "Las fechas se basan en tus respuestas correctas: 1, 3, 7, 14, 30, 60 y 120 días. Los términos marcados como difíciles se practican por separado en el área de términos difíciles.",
"courseFlowPracticeTitle": "Práctica libre", "courseFlowPracticeTitle": "Práctica libre",
"courseFlowPracticeDescription": "Lecciones completadas para repasar con calma fuera del camino obligatorio.", "courseFlowPracticeDescription": "Lecciones completadas para repasar con calma fuera del camino obligatorio.",
"courseFlowPracticeEmpty": "En cuanto completes tus primeras lecciones, aparecerán aquí para práctica libre.", "courseFlowPracticeEmpty": "En cuanto completes tus primeras lecciones, aparecerán aquí para práctica libre.",

View File

@@ -750,9 +750,14 @@
"courseFlowBlockTitle": "Bloc actuel", "courseFlowBlockTitle": "Bloc actuel",
"courseFlowBlockDescription": "C'est là que se situe la prochaine progression régulière du cours.", "courseFlowBlockDescription": "C'est là que se situe la prochaine progression régulière du cours.",
"courseFlowBlockEmpty": "Le bloc en cours est déjà terminé ou il n'y a actuellement aucune leçon en bloc ouverte.", "courseFlowBlockEmpty": "Le bloc en cours est déjà terminé ou il n'y a actuellement aucune leçon en bloc ouverte.",
"courseFlowIntensiveTitle": "Phase intensive due", "courseFlowIntensiveTitle": "Prochaines révisions quotidiennes",
"courseFlowIntensiveDescription": "Répétition condensée dès que le bloc devant lui est largement en place.", "courseFlowIntensiveDescription": "Les dates de révision de vos termes dépendent de vos réponses correctes précédentes.",
"courseFlowIntensiveEmpty": "Il ny a actuellement aucune nouvelle phase intensive activée.", "courseFlowIntensiveEmpty": "Vous pouvez consulter les prochaines dates ici.",
"courseFlowIntensiveStatusAction": "Vérifier les prochaines révisions",
"courseFlowIntensiveStatusTitle": "Prochaines révisions de vocabulaire",
"courseFlowIntensiveStatusAllDone": "Aucune autre révision de vocabulaire nest actuellement prévue pour les leçons terminées.",
"courseFlowIntensiveStatusProgress": "{count} termes",
"courseFlowIntensiveStatusNoDate": "Les dates reposent sur vos réponses correctes : 1, 3, 7, 14, 30, 60 et 120 jours. Les termes marqués difficiles sont travaillés séparément dans la zone des termes difficiles.",
"courseFlowPracticeTitle": "Approfondissement gratuit", "courseFlowPracticeTitle": "Approfondissement gratuit",
"courseFlowPracticeDescription": "Cours terminés pour une formation de suivi occasionnelle en dehors du parcours obligatoire.", "courseFlowPracticeDescription": "Cours terminés pour une formation de suivi occasionnelle en dehors du parcours obligatoire.",
"courseFlowPracticeEmpty": "Dès que vous aurez terminé les premières leçons, elles apparaîtront ici pour une formation complémentaire gratuite.", "courseFlowPracticeEmpty": "Dès que vous aurez terminé les premières leçons, elles apparaîtront ici pour une formation complémentaire gratuite.",

View File

@@ -196,17 +196,7 @@
<p>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveDescription') }}</p> <p>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveDescription') }}</p>
</div> </div>
</div> </div>
<div v-if="hardVocabCount > 0" class="course-flow-card__list"> <p class="course-flow-card__empty">{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveEmpty') }}</p>
<button
type="button"
class="course-flow-lesson"
@click="openHardPractice"
>
<strong>{{ $t('socialnetwork.vocab.courses.startHardVocabTrainer', { count: hardVocabCount }) }}</strong>
<span>{{ $t('socialnetwork.vocab.courses.practiceInTrainer') }}</span>
</button>
</div>
<p v-else class="course-flow-card__empty">{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveEmpty') }}</p>
<button type="button" class="course-flow-card__status" @click="showIntensiveStatusDialog = true"> <button type="button" class="course-flow-card__status" @click="showIntensiveStatusDialog = true">
{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusAction') }} {{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusAction') }}
</button> </button>