feat(VocabPracticeDialog, VocabCourseView): implement SRS rating feature and enhance user feedback
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s

- Added SRS rating buttons in VocabPracticeDialog to allow users to rate their confidence after answering vocabulary questions.
- Updated methods to handle SRS ratings and integrated them into the review process, improving spaced repetition feedback.
- Enhanced UI with new styles for SRS rating buttons and updated translations for SRS-related terms in multiple languages.
- Modified VocabCourseView to display appropriate introductory text based on SRS due items, improving user guidance.
This commit is contained in:
Torsten Schulz (local)
2026-04-17 09:27:29 +02:00
parent e2c1147d75
commit 8be215761d
5 changed files with 149 additions and 16 deletions

View File

@@ -47,6 +47,22 @@
</div>
</div>
<div v-if="showSrsRatingButtons" class="srs-rating">
<div class="srs-rating__title">{{ $t('socialnetwork.vocab.practice.srsRateTitle') }}</div>
<button
v-for="option in srsRatingOptions"
:key="option.value"
type="button"
class="srs-rating__button"
:class="`srs-rating__button--${option.value}`"
:disabled="locked"
@click="submitSrsRating(option.value)"
>
<strong>{{ option.label }}</strong>
<span>{{ option.hint }}</span>
</button>
</div>
<div v-if="!answered" class="answerArea">
<div v-if="simpleMode" class="choices">
<button
@@ -165,11 +181,45 @@ export default {
},
showNextButton() {
// Nur bei falscher Antwort auf "Weiter" warten
return this.answered && !this.lastCorrect;
return this.answered && !this.lastCorrect && !this.srsMode;
},
showSkipButton() {
return !this.answered;
},
showSrsRatingButtons() {
return this.srsMode && this.answered && !this.locked;
},
srsRatingOptions() {
if (!this.answered) {
return [];
}
if (!this.lastCorrect) {
return [
{
value: 'again',
label: this.$t('socialnetwork.vocab.practice.srsAgain'),
hint: this.$t('socialnetwork.vocab.practice.srsAgainHint')
}
];
}
return [
{
value: 'hard',
label: this.$t('socialnetwork.vocab.practice.srsHard'),
hint: this.$t('socialnetwork.vocab.practice.srsHardHint')
},
{
value: 'good',
label: this.$t('socialnetwork.vocab.practice.srsGood'),
hint: this.$t('socialnetwork.vocab.practice.srsGoodHint')
},
{
value: 'easy',
label: this.$t('socialnetwork.vocab.practice.srsEasy'),
hint: this.$t('socialnetwork.vocab.practice.srsEasyHint')
}
];
},
},
methods: {
open({ languageId, chapterId, lessonId, courseId, initialPool = null, srsMode = false, onClose = null }) {
@@ -376,7 +426,7 @@ export default {
wrong: Number(st.w) || 0
};
})
.filter((entry) => entry.attempts < PRACTICE_MIN_EXPOSURES && !recent.has(entry.item.id))
.filter((entry) => entry.attempts < (this.srsMode ? 1 : PRACTICE_MIN_EXPOSURES) && !recent.has(entry.item.id))
.sort((a, b) => {
if (a.attempts !== b.attempts) return a.attempts - b.attempts;
if (a.wrong !== b.wrong) return b.wrong - a.wrong;
@@ -433,18 +483,19 @@ export default {
// ignore autoplay issues
}
},
reportSrsReview(isCorrect) {
reportSrsReview(isCorrect, rating = null) {
if (!this.current || !this.openParams?.courseId) {
return;
return Promise.resolve();
}
apiClient.post('/api/vocab/srs/review', {
return apiClient.post('/api/vocab/srs/review', {
courseId: this.openParams.courseId || this.current.courseId,
lessonId: this.openParams.lessonId || this.current.lessonId || null,
itemKey: this.current.itemKey || null,
learning: this.current.learning,
reference: this.current.reference,
direction: this.direction,
correct: Boolean(isCorrect)
correct: Boolean(isCorrect),
rating
}).catch((error) => {
console.warn('[VocabPracticeDialog] SRS review could not be saved:', error);
});
@@ -454,7 +505,9 @@ export default {
this.lastCorrect = isCorrect;
if (isCorrect) this.correctCount += 1;
else this.wrongCount += 1;
if (!this.srsMode) {
this.reportSrsReview(isCorrect);
}
const id = this.current?.id;
if (!id) return;
@@ -472,12 +525,20 @@ export default {
this.lastIds.unshift(id);
this.lastIds = this.lastIds.slice(0, 3);
},
async submitSrsRating(rating) {
if (!this.srsMode || !this.answered || this.locked) {
return;
}
this.locked = true;
await this.reportSrsReview(this.lastCorrect, rating);
this.next();
},
submitChoice(opt) {
if (this.locked) return;
const ok = this.acceptableAnswers.map(this.normalize).includes(this.normalize(opt));
this.markResult(ok);
this.playSound(ok);
if (ok) {
if (ok && !this.srsMode) {
// Direkt weiter zur nächsten Frage (kein Klick nötig)
this.locked = true;
this.autoAdvanceTimer = setTimeout(() => {
@@ -492,7 +553,7 @@ export default {
const ok = this.acceptableAnswers.map(this.normalize).includes(ans);
this.markResult(ok);
this.playSound(ok);
if (ok) {
if (ok && !this.srsMode) {
this.locked = true;
this.autoAdvanceTimer = setTimeout(() => {
this.autoAdvanceTimer = null;
@@ -585,6 +646,46 @@ export default {
.controls {
margin-top: 12px;
}
.srs-rating {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
gap: 8px;
margin: 12px 0;
}
.srs-rating__title {
grid-column: 1 / -1;
font-size: 0.82rem;
font-weight: 700;
color: var(--color-text-secondary, #5f554e);
}
.srs-rating__button {
display: flex;
flex-direction: column;
gap: 2px;
align-items: flex-start;
padding: 8px 10px;
border: 1px solid var(--color-border, #d7d0c8);
border-radius: 10px;
background: rgba(255, 255, 255, 0.9);
cursor: pointer;
text-align: left;
}
.srs-rating__button span {
font-size: 0.74rem;
color: var(--color-text-secondary, #6b625b);
}
.srs-rating__button--again {
border-color: rgba(198, 75, 75, 0.45);
}
.srs-rating__button--hard {
border-color: rgba(210, 153, 74, 0.5);
}
.srs-rating__button--good {
border-color: rgba(90, 145, 95, 0.45);
}
.srs-rating__button--easy {
border-color: rgba(78, 139, 188, 0.45);
}
.feedback {
padding: 10px;
border: 1px solid #ccc;

View File

@@ -731,7 +731,8 @@
"srsEyebrow": "Dugay nga memorya",
"srsTitle": "{count} ka termino ang angay karon",
"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"
"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."
},
"title": "Trainer sa bokabularyo",
"description": "Paghimo og pinulongans (or subscribe aron them) ug share them uban sa friends.",
@@ -787,7 +788,16 @@
"acceptable": "Acceptable answers:",
"stats": "Stats",
"success": "Malampuson",
"fail": "Fail"
"fail": "Fail",
"srsRateTitle": "Unsa ka lig-on sa imong pagbati?",
"srsAgain": "Usab",
"srsAgainHint": "balikon dayon",
"srsHard": "Lisod",
"srsHardHint": "mubo nga interval",
"srsGood": "Maayo",
"srsGoodHint": "normal nga iskedyul",
"srsEasy": "Sayon",
"srsEasyHint": "mas layo nga interval"
},
"search": {
"open": "Pangita",

View File

@@ -453,7 +453,16 @@
"acceptable": "Mögliche richtige Übersetzungen:",
"stats": "Statistik",
"success": "Erfolg",
"fail": "Misserfolg"
"fail": "Misserfolg",
"srsRateTitle": "Wie sicher war das?",
"srsAgain": "Nochmal",
"srsAgainHint": "sehr bald wiederholen",
"srsHard": "Schwer",
"srsHardHint": "kurzes Intervall",
"srsGood": "Gut",
"srsGoodHint": "normal planen",
"srsEasy": "Leicht",
"srsEasyHint": "größerer Abstand"
},
"search": {
"open": "Suche",
@@ -813,7 +822,8 @@
"srsEyebrow": "Langzeitgedächtnis",
"srsTitle": "{count} Begriffe sind heute fällig",
"srsIntro": "Diese Wiederholung kommt aus dem SRS-Plan einzelner Begriffe. Sie hat Vorrang vor neuem Stoff, weil sie kurz vor dem Vergessen stabilisiert.",
"srsStart": "Tageswiederholung starten"
"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."
}
}
}

View File

@@ -453,7 +453,16 @@
"acceptable": "Acceptable answers:",
"stats": "Stats",
"success": "Success",
"fail": "Fail"
"fail": "Fail",
"srsRateTitle": "How solid did it feel?",
"srsAgain": "Again",
"srsAgainHint": "repeat very soon",
"srsHard": "Hard",
"srsHardHint": "short interval",
"srsGood": "Good",
"srsGoodHint": "normal schedule",
"srsEasy": "Easy",
"srsEasyHint": "longer interval"
},
"search": {
"open": "Search",
@@ -813,7 +822,8 @@
"srsEyebrow": "Long-term memory",
"srsTitle": "{count} terms are due today",
"srsIntro": "This review comes from the SRS schedule of individual terms. It should come before new material because it stabilizes items close to forgetting.",
"srsStart": "Start 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."
}
}
}

View File

@@ -69,7 +69,9 @@
<div v-if="todayRecommendedSteps.length > 0" class="course-today-plan">
<h4 class="course-today-plan__title">{{ $t('socialnetwork.vocab.courses.courseTodayPlanTitle') }}</h4>
<p class="course-today-plan__intro">
{{ dueReviewLessons.length > 0
{{ srsDueCount > 0
? $t('socialnetwork.vocab.courses.courseTodayPlanIntroSrs')
: dueReviewLessons.length > 0
? $t('socialnetwork.vocab.courses.courseTodayPlanIntro')
: $t('socialnetwork.vocab.courses.courseTodayPlanIntroNoDueReview') }}
</p>