feat(VocabPracticeDialog, VocabCourseView): implement SRS rating feature and enhance user feedback
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
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:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user