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;
this.reportSrsReview(isCorrect);
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;