feat(vocab): stabilize daily batch handling in SRS session and improve due item display
All checks were successful
Deploy to production / deploy (push) Successful in 3m8s
All checks were successful
Deploy to production / deploy (push) Successful in 3m8s
This commit is contained in:
@@ -140,6 +140,7 @@ import apiClient from '@/utils/axios.js';
|
||||
import { normalizeComparableWithNumberWords } from '@/utils/numberAnswerVariants.js';
|
||||
|
||||
const PRACTICE_MIN_EXPOSURES = 3;
|
||||
// The original daily batch remains stable when the dialog is reopened.
|
||||
const SRS_SESSION_STORAGE_VERSION = 2;
|
||||
const HARD_REQUIRED_CONSECUTIVE_CORRECT = 5;
|
||||
const MAX_DAILY_DUE = 50;
|
||||
@@ -502,10 +503,7 @@ export default {
|
||||
const dueIds = (this.pool || []).map((it) => it.id);
|
||||
const stored = this.loadSrsSession();
|
||||
|
||||
// If the previous session is already complete, start fresh (new batch of due items).
|
||||
if (stored && Number(stored.initialTotalDue || 0) > 0 && Array.isArray(stored.doneIds) && stored.doneIds.length >= stored.initialTotalDue) {
|
||||
this.srsSession = null;
|
||||
} else if (stored) {
|
||||
if (stored) {
|
||||
this.srsSession = stored;
|
||||
} else {
|
||||
this.srsSession = null;
|
||||
@@ -543,16 +541,23 @@ export default {
|
||||
try { this.saveSrsSession(); } catch (_) {}
|
||||
}
|
||||
|
||||
const initialDueIds = Array.isArray(this.srsSession.initialDueIds)
|
||||
? this.srsSession.initialDueIds.slice(0, MAX_DAILY_DUE)
|
||||
: [];
|
||||
const doneSet = new Set(Array.isArray(this.srsSession.doneIds) ? this.srsSession.doneIds : []);
|
||||
const finalReviewIds = Array.isArray(this.srsSession.finalReviewIds) ? this.srsSession.finalReviewIds : [];
|
||||
const finalReviewSet = new Set(finalReviewIds);
|
||||
const availableIds = new Set(dueIds);
|
||||
// Never fill a partly completed daily batch with additional overdue
|
||||
// cards. The 50 cards chosen when the session began remain today's
|
||||
// batch; answered cards reduce its remaining count.
|
||||
this.srsQueueIds = [
|
||||
...dueIds.filter((id) => !doneSet.has(id) && !finalReviewSet.has(id)),
|
||||
...finalReviewIds.filter((id) => !doneSet.has(id) && availableIds.has(id))
|
||||
...initialDueIds.filter((id) => availableIds.has(id) && !doneSet.has(id) && !finalReviewSet.has(id)),
|
||||
...finalReviewIds.filter((id) => initialDueIds.includes(id) && !doneSet.has(id) && availableIds.has(id))
|
||||
];
|
||||
// Fallback: if SRS mode but queue is empty (e.g. mismatch between stored session and current pool), fall back to pool order
|
||||
if (this.srsMode && Array.isArray(this.srsQueueIds) && this.srsQueueIds.length === 0) {
|
||||
// Only a corrupted session without an original batch may use the pool
|
||||
// as fallback. An empty, completed batch must remain empty today.
|
||||
if (this.srsMode && initialDueIds.length === 0) {
|
||||
const fallbackIds = (this.pool || []).map((it) => it.id).filter(Boolean);
|
||||
if (fallbackIds.length > 0) {
|
||||
const limited = fallbackIds.slice(0, MAX_DAILY_DUE);
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
"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.",
|
||||
"courseFlowIntensiveStatusDue": "Karon / nalangan",
|
||||
"courseFlowIntensiveStatusDue": "Nahibilin karon: {remaining} sa {quota} giplano · kinatibuk-ang angay: {total}",
|
||||
"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",
|
||||
@@ -766,9 +766,9 @@
|
||||
"quickReviewPromptTarget": "Type sa target pinulongan: \"{term}\"",
|
||||
"quickReviewAcknowledge": "Read, continue",
|
||||
"courseTodayPlanIntroNoDueReview": "Walay angay nga mubo nga balik-balik karon. Makita nimo ang sunod nga makatarunganon nga lakang sa block (limitado sa kabug-aton), unya ang intensive kung naay. Ang mubo nga balik-balik mobalik sa 1/3/7 ka adlaw.",
|
||||
"srsDueStat": "SRS angay: {count}",
|
||||
"srsDueStat": "Adlaw-adlaw nga balik-balik: {scheduled} sa {total} ka termino",
|
||||
"srsEyebrow": "Dugay nga memorya",
|
||||
"srsTitle": "{count} ka termino ang angay karon",
|
||||
"srsTitle": "Karon: {scheduled} sa {total} ka angay balik-balikon",
|
||||
"srsIntro": "Kini nga balik-balik gikan sa SRS nga plano sa matag pulong. Una kini kaysa bag-ong materyal kay nagpalig-on kini sa mga pulong nga hapit malimtan.",
|
||||
"srsStart": "Sugdi ang daily review",
|
||||
"courseTodayPlanIntroSrs": "Didaktik nga han-ay: una ang SRS daily review sa tagsa-tagsa ka pulong. Human ana, mubo nga review, padayon sa block, ug intensive review kung kinahanglan. Mao ni ang pagpalig-on sa daan nga materyal sa dili pa modugang ug bag-o."
|
||||
|
||||
@@ -777,7 +777,7 @@
|
||||
"courseFlowIntensiveStatusAction": "Nächste Wiederholungen prüfen",
|
||||
"courseFlowIntensiveStatusTitle": "Nächste Vokabelwiederholungen",
|
||||
"courseFlowIntensiveStatusAllDone": "Für abgeschlossene Lektionen sind derzeit keine weiteren Vokabelwiederholungen geplant.",
|
||||
"courseFlowIntensiveStatusDue": "Heute / überfällig",
|
||||
"courseFlowIntensiveStatusDue": "Heute noch: {remaining} von {quota} eingeplant · insgesamt fällig: {total}",
|
||||
"courseFlowIntensiveStatusProgress": "{count} Begriffe",
|
||||
"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",
|
||||
@@ -876,9 +876,9 @@
|
||||
"reviewTimeNow": "jetzt",
|
||||
"reviewTimeTomorrow": "morgen",
|
||||
"reviewTimeInDays": "in {count} Tagen",
|
||||
"srsDueStat": "Tageswiederholung: {scheduled} Begriffe",
|
||||
"srsDueStat": "Tageswiederholung: {scheduled} von {total} Begriffen",
|
||||
"srsEyebrow": "Langzeitgedächtnis",
|
||||
"srsTitle": "Heute: {scheduled} fällige Begriffe",
|
||||
"srsTitle": "Heute: {scheduled} von {total} fälligen Begriffen",
|
||||
"srsIntro": "Diese Wiederholung kommt aus dem SRS-Plan einzelner Begriffe. Pro Tag werden höchstens 50 Begriffe eingeplant; weitere fällige Begriffe bleiben für die nächste Tageswiederholung erhalten.",
|
||||
"srsStart": "Tageswiederholung starten",
|
||||
"courseTodayPlanIntroSrs": "Didaktische Reihenfolge: Zuerst die fällige SRS-Tageswiederholung einzelner Begriffe. Danach kommen Kurz-Wiederholungen, Blockfortschritt und ggf. intensive Wiederholung. So wird altes Material stabilisiert, bevor neues Material dazukommt."
|
||||
|
||||
@@ -777,7 +777,7 @@
|
||||
"courseFlowIntensiveStatusAction": "Check upcoming reviews",
|
||||
"courseFlowIntensiveStatusTitle": "Upcoming vocabulary reviews",
|
||||
"courseFlowIntensiveStatusAllDone": "No further vocabulary reviews are currently scheduled for completed lessons.",
|
||||
"courseFlowIntensiveStatusDue": "Today / overdue",
|
||||
"courseFlowIntensiveStatusDue": "Remaining today: {remaining} of {quota} planned · total due: {total}",
|
||||
"courseFlowIntensiveStatusProgress": "{count} terms",
|
||||
"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",
|
||||
@@ -876,9 +876,9 @@
|
||||
"reviewTimeNow": "now",
|
||||
"reviewTimeTomorrow": "tomorrow",
|
||||
"reviewTimeInDays": "in {count} days",
|
||||
"srsDueStat": "Daily review: {scheduled} terms",
|
||||
"srsDueStat": "Daily review: {scheduled} of {total} terms",
|
||||
"srsEyebrow": "Long-term memory",
|
||||
"srsTitle": "Today: {scheduled} due terms",
|
||||
"srsTitle": "Today: {scheduled} of {total} due terms",
|
||||
"srsIntro": "This review comes from the SRS schedule of individual terms. At most 50 terms are planned per day; any additional due terms remain for the next daily review.",
|
||||
"srsStart": "Start daily review",
|
||||
"courseTodayPlanIntroSrs": "Pedagogical order: start with the due SRS daily review for individual terms. Then quick reviews, block progress, and intensive review if needed. This stabilizes old material before new material is added."
|
||||
|
||||
@@ -756,7 +756,7 @@
|
||||
"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.",
|
||||
"courseFlowIntensiveStatusDue": "Hoy / atrasado",
|
||||
"courseFlowIntensiveStatusDue": "Pendientes hoy: {remaining} de {quota} planificados · total pendientes: {total}",
|
||||
"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",
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" @click="orderParty()">
|
||||
<button type="button" @click="orderParty()" :disabled="isOrderingParty">
|
||||
{{ $t('falukant.reputation.party.order') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -224,6 +224,7 @@ export default {
|
||||
selectedNobilityIds: [],
|
||||
servantRatio: 50,
|
||||
relationshipId: null,
|
||||
isOrderingParty: false,
|
||||
weddingCandidates: [],
|
||||
inProgressParties: [],
|
||||
completedParties: [],
|
||||
@@ -317,15 +318,23 @@ export default {
|
||||
this.nobilityTitles = await apiClient.get('/api/falukant/nobility/titels').then(r => r.data)
|
||||
},
|
||||
async orderParty() {
|
||||
await apiClient.post('/api/falukant/party', {
|
||||
partyTypeId: this.newPartyTypeId,
|
||||
musicId: this.musicId,
|
||||
banquetteId: this.banquetteId,
|
||||
nobilityIds: this.selectedNobilityIds.map(n => n.id ?? n),
|
||||
servantRatio: this.servantRatio,
|
||||
relationshipId: this.relationshipId
|
||||
});
|
||||
this.toggleNewPartyView();
|
||||
if (this.isOrderingParty) return;
|
||||
this.isOrderingParty = true;
|
||||
try {
|
||||
await apiClient.post('/api/falukant/party', {
|
||||
partyTypeId: this.newPartyTypeId,
|
||||
musicId: this.musicId,
|
||||
banquetteId: this.banquetteId,
|
||||
nobilityIds: this.selectedNobilityIds.map(n => n.id ?? n),
|
||||
servantRatio: this.servantRatio,
|
||||
relationshipId: this.relationshipId
|
||||
});
|
||||
await this.loadParties();
|
||||
this.relationshipId = null;
|
||||
this.toggleNewPartyView();
|
||||
} finally {
|
||||
this.isOrderingParty = false;
|
||||
}
|
||||
},
|
||||
getPartyDate(createdAt) {
|
||||
// Feste finden 1 Tag nach der Bestellung statt
|
||||
|
||||
@@ -317,11 +317,10 @@
|
||||
<section class="dialog intensive-status-dialog" role="dialog" aria-modal="true" @click.stop>
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusTitle') }}</h3>
|
||||
<p class="intensive-status-dialog__hint">{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusNoDate') }}</p>
|
||||
<p v-if="srsDailyCount === 0 && srsUpcoming.length === 0">{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusAllDone') }}</p>
|
||||
<p v-if="srsDueCount === 0 && srsUpcoming.length === 0">{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusAllDone') }}</p>
|
||||
<ul v-else class="intensive-status-dialog__list">
|
||||
<li v-if="srsDailyCount > 0" class="intensive-status-dialog__item--due">
|
||||
<span>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusDue') }}</span>
|
||||
<strong>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusProgress', { count: srsDailyCount }) }}</strong>
|
||||
<li v-if="srsDueCount > 0" class="intensive-status-dialog__item--due">
|
||||
<span>{{ $t('socialnetwork.vocab.courses.courseFlowIntensiveStatusDue', { remaining: srsDailyCount, quota: srsDailyQuota, total: srsDueCount }) }}</span>
|
||||
</li>
|
||||
<li v-for="entry in srsUpcoming" :key="entry.date">
|
||||
<span>{{ formatSrsDate(entry.date) }}</span>
|
||||
@@ -397,6 +396,7 @@ export default {
|
||||
srsDueTotal: 0,
|
||||
srsDailyLimit: 50,
|
||||
srsTodayRemaining: null,
|
||||
srsTodayQuota: null,
|
||||
srsUpcoming: [],
|
||||
srsLoading: false,
|
||||
showIntensiveStatusDialog: false,
|
||||
@@ -449,9 +449,17 @@ export default {
|
||||
return Array.isArray(this.srsDueItems) ? this.srsDueItems.length : 0;
|
||||
},
|
||||
srsDailyCount() {
|
||||
// Der Server ist die maßgebliche Quelle für fällige Begriffe. Ein alter
|
||||
// localStorage-Sitzungsstand darf eine neue oder erneut fällige
|
||||
// Tageswiederholung nicht unsichtbar machen.
|
||||
// A valid current-day session represents the fixed daily batch. Without
|
||||
// one, the server's due count determines the first batch (max. 50).
|
||||
if (Number.isFinite(Number(this.srsTodayRemaining))) {
|
||||
return Math.max(0, Number(this.srsTodayRemaining));
|
||||
}
|
||||
return Math.min(this.srsDueCount, this.srsDailyLimit);
|
||||
},
|
||||
srsDailyQuota() {
|
||||
if (Number.isFinite(Number(this.srsTodayQuota))) {
|
||||
return Math.max(0, Number(this.srsTodayQuota));
|
||||
}
|
||||
return Math.min(this.srsDueCount, this.srsDailyLimit);
|
||||
},
|
||||
hardVocabCount() {
|
||||
@@ -588,13 +596,18 @@ export default {
|
||||
&& session.dateKey === this.getLocalDateKey();
|
||||
if (!isCurrentSession) {
|
||||
this.srsTodayRemaining = null;
|
||||
this.srsTodayQuota = null;
|
||||
return;
|
||||
}
|
||||
const total = Math.max(0, Number(session.initialTotalDue) || 0);
|
||||
const done = new Set(Array.isArray(session.doneIds) ? session.doneIds : []).size;
|
||||
const initialIds = new Set(Array.isArray(session.initialDueIds) ? session.initialDueIds : []);
|
||||
const done = new Set((Array.isArray(session.doneIds) ? session.doneIds : [])
|
||||
.filter((id) => initialIds.has(id))).size;
|
||||
this.srsTodayRemaining = Math.max(0, total - done);
|
||||
this.srsTodayQuota = total;
|
||||
} catch (_) {
|
||||
this.srsTodayRemaining = null;
|
||||
this.srsTodayQuota = null;
|
||||
}
|
||||
},
|
||||
async refreshHardVocabList() {
|
||||
|
||||
Reference in New Issue
Block a user