feat(VocabService, VocabPracticeDialog, VocabLessonView): enhance vocabulary training logic and UI feedback
All checks were successful
Deploy to production / deploy (push) Successful in 2m17s
All checks were successful
Deploy to production / deploy (push) Successful in 2m17s
- Introduced methods for improved text analysis and validation in VocabService, including `_wordCount` and `_looksLikeFragmentMismatch`, to better assess learning and reference pairs. - Updated VocabPracticeDialog to display submitted answers and correct solutions, enhancing user feedback during practice sessions. - Enhanced VocabLessonView to ensure only trainable vocabulary pairs are processed, improving the quality of vocabulary training. - Added localization entries for new UI elements in both English and German, ensuring clarity in user interactions.
This commit is contained in:
@@ -92,9 +92,35 @@ export default class VocabService {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._looksLikeFragmentMismatch(learning, reference)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this._isInstructionLikeText(learning) && !this._isInstructionLikeText(reference);
|
||||
}
|
||||
|
||||
_wordCount(value) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/[\p{P}\p{S}]+/gu, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.length;
|
||||
}
|
||||
|
||||
_looksLikeFragmentMismatch(left, right) {
|
||||
const leftWords = this._wordCount(left);
|
||||
const rightWords = this._wordCount(right);
|
||||
const leftText = String(left || '').trim();
|
||||
const rightText = String(right || '').trim();
|
||||
const leftLooksSentence = leftWords >= 3 || /[?!.].+\S/.test(leftText);
|
||||
const rightLooksSentence = rightWords >= 3 || /[?!.].+\S/.test(rightText);
|
||||
const leftLooksShortFragment = leftWords <= 1 && leftText.length <= 12;
|
||||
const rightLooksShortFragment = rightWords <= 1 && rightText.length <= 12;
|
||||
|
||||
return (leftLooksShortFragment && rightLooksSentence) || (rightLooksShortFragment && leftLooksSentence);
|
||||
}
|
||||
|
||||
_calculateSrsSchedule(item, { correct, rating = null } = {}) {
|
||||
const now = new Date();
|
||||
const previousStage = Math.max(0, Number(item?.stage) || 0);
|
||||
|
||||
@@ -42,15 +42,20 @@
|
||||
<div v-if="lastCorrect">{{ $t('socialnetwork.vocab.practice.correct') }}</div>
|
||||
<div v-else>
|
||||
{{ $t('socialnetwork.vocab.practice.wrong') }}
|
||||
<div class="answers">
|
||||
<div class="answersTitle">{{ $t('socialnetwork.vocab.practice.acceptable') }}</div>
|
||||
<ul>
|
||||
<li v-for="a in acceptableAnswers" :key="a">{{ a }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="answered && !lastCorrect" class="solution-card">
|
||||
<div v-if="submittedAnswer" class="submitted-answer">
|
||||
<span>{{ $t('socialnetwork.vocab.practice.yourAnswer') }}</span>
|
||||
<strong>{{ submittedAnswer }}</strong>
|
||||
</div>
|
||||
<div class="answersTitle">{{ $t('socialnetwork.vocab.practice.correctSolutions') }}</div>
|
||||
<ul>
|
||||
<li v-for="a in visibleCorrectAnswers" :key="a">{{ a }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="showSrsRatingButtons" class="srs-rating">
|
||||
<div class="srs-rating__title">{{ $t('socialnetwork.vocab.practice.srsRateTitle') }}</div>
|
||||
<button
|
||||
@@ -169,6 +174,7 @@ export default {
|
||||
current: null, // { id, learning, reference }
|
||||
direction: 'L2R', // L2R: learning->reference, R2L: reference->learning
|
||||
acceptableAnswers: [],
|
||||
submittedAnswer: '',
|
||||
choiceOptions: [],
|
||||
typedAnswer: '',
|
||||
answered: false,
|
||||
@@ -211,6 +217,17 @@ export default {
|
||||
showSrsRatingButtons() {
|
||||
return this.srsMode && this.answered && !this.locked;
|
||||
},
|
||||
visibleCorrectAnswers() {
|
||||
const answers = Array.isArray(this.acceptableAnswers) ? this.acceptableAnswers.filter(Boolean) : [];
|
||||
if (answers.length > 0) {
|
||||
return answers;
|
||||
}
|
||||
if (!this.current) {
|
||||
return [];
|
||||
}
|
||||
const fallback = this.direction === 'L2R' ? this.current.reference : this.current.learning;
|
||||
return this.expandAnswerVariants(fallback);
|
||||
},
|
||||
srsRatingOptions() {
|
||||
if (!this.answered) {
|
||||
return [];
|
||||
@@ -429,8 +446,31 @@ export default {
|
||||
if (!learning || !reference || this.normalize(learning) === this.normalize(reference)) {
|
||||
return false;
|
||||
}
|
||||
if (this.looksLikeFragmentMismatch(learning, reference)) {
|
||||
return false;
|
||||
}
|
||||
return !this.isInstructionLikeText(learning) && !this.isInstructionLikeText(reference);
|
||||
},
|
||||
wordCount(value) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/[\p{P}\p{S}]+/gu, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.length;
|
||||
},
|
||||
looksLikeFragmentMismatch(left, right) {
|
||||
const leftWords = this.wordCount(left);
|
||||
const rightWords = this.wordCount(right);
|
||||
const leftText = String(left || '').trim();
|
||||
const rightText = String(right || '').trim();
|
||||
const leftLooksSentence = leftWords >= 3 || /[?!.].+\S/.test(leftText);
|
||||
const rightLooksSentence = rightWords >= 3 || /[?!.].+\S/.test(rightText);
|
||||
const leftLooksShortFragment = leftWords <= 1 && leftText.length <= 12;
|
||||
const rightLooksShortFragment = rightWords <= 1 && rightText.length <= 12;
|
||||
|
||||
return (leftLooksShortFragment && rightLooksSentence) || (rightLooksShortFragment && leftLooksSentence);
|
||||
},
|
||||
normalizePool(items = []) {
|
||||
const seen = new Set();
|
||||
return (Array.isArray(items) ? items : [])
|
||||
@@ -458,6 +498,7 @@ export default {
|
||||
this.current = null;
|
||||
this.direction = this.openParams?.lessonId ? 'L2R' : (Math.random() < 0.5 ? 'L2R' : 'R2L');
|
||||
this.acceptableAnswers = [];
|
||||
this.submittedAnswer = '';
|
||||
this.choiceOptions = [];
|
||||
this.typedAnswer = '';
|
||||
this.answered = false;
|
||||
@@ -473,6 +514,7 @@ export default {
|
||||
this.answered = false;
|
||||
this.lastCorrect = false;
|
||||
this.typedAnswer = '';
|
||||
this.submittedAnswer = '';
|
||||
|
||||
if (!this.pool || this.pool.length === 0) return;
|
||||
|
||||
@@ -746,6 +788,7 @@ export default {
|
||||
},
|
||||
submitChoice(opt) {
|
||||
if (this.locked) return;
|
||||
this.submittedAnswer = String(opt || '').trim();
|
||||
const ok = this.acceptableAnswers.map(this.normalize).includes(this.normalize(opt));
|
||||
this.markResult(ok);
|
||||
this.playSound(ok);
|
||||
@@ -760,6 +803,7 @@ export default {
|
||||
},
|
||||
submitTyped() {
|
||||
if (this.locked) return;
|
||||
this.submittedAnswer = String(this.typedAnswer || '').trim();
|
||||
const ans = this.normalize(this.typedAnswer);
|
||||
const ok = this.acceptableAnswers.map(this.normalize).includes(ans);
|
||||
this.markResult(ok);
|
||||
@@ -927,6 +971,25 @@ export default {
|
||||
background: #ffecec;
|
||||
border-color: #d33;
|
||||
}
|
||||
.solution-card {
|
||||
margin: 0 0 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #d33;
|
||||
background: #fff8f8;
|
||||
}
|
||||
.solution-card ul {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.submitted-answer {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
margin-bottom: 10px;
|
||||
color: #5f554e;
|
||||
}
|
||||
.submitted-answer strong {
|
||||
color: #2b2520;
|
||||
}
|
||||
.answersTitle {
|
||||
margin-top: 6px;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -451,6 +451,8 @@
|
||||
"correct": "Richtig!",
|
||||
"wrong": "Falsch.",
|
||||
"acceptable": "Mögliche richtige Übersetzungen:",
|
||||
"correctSolutions": "Richtige Antwort:",
|
||||
"yourAnswer": "Deine Antwort:",
|
||||
"stats": "Statistik",
|
||||
"dueToday": "Heute fällig",
|
||||
"done": "Erledigt",
|
||||
|
||||
@@ -451,6 +451,8 @@
|
||||
"correct": "Correct!",
|
||||
"wrong": "Wrong.",
|
||||
"acceptable": "Acceptable answers:",
|
||||
"correctSolutions": "Correct answer:",
|
||||
"yourAnswer": "Your answer:",
|
||||
"stats": "Stats",
|
||||
"dueToday": "Due today",
|
||||
"done": "Done",
|
||||
|
||||
@@ -1452,7 +1452,7 @@ export default {
|
||||
const oriented = orientPair(entry?.learning, entry?.reference);
|
||||
const reference = String(oriented.reference || '').trim();
|
||||
const learning = String(oriented.learning || '').trim();
|
||||
if (!reference) return;
|
||||
if (!this.isTrainableLessonVocabPair(learning, reference)) return;
|
||||
const key = this.normalizeLessonVocabTerm(reference);
|
||||
if (!vocabByReference.has(key)) {
|
||||
const variants = new Set();
|
||||
@@ -1523,7 +1523,7 @@ export default {
|
||||
return { from, to, page, pages: totalPages, total: n };
|
||||
},
|
||||
trainableLessonVocab() {
|
||||
return this.lessonVocab.filter((entry) => entry.learning && entry.reference && entry.learning !== entry.reference);
|
||||
return this.lessonVocab.filter((entry) => this.isTrainableLessonVocabPair(entry.learning, entry.reference));
|
||||
},
|
||||
lessonDidactics() {
|
||||
return this.lesson?.didactics || {
|
||||
@@ -1610,7 +1610,7 @@ export default {
|
||||
const oriented = orientPair(target, gloss);
|
||||
const t = String(oriented.target || '').trim();
|
||||
const g = String(oriented.gloss || '').trim();
|
||||
if (!t || !g) return;
|
||||
if (!this.isTrainableLessonVocabPair(g, t)) return;
|
||||
const key = `${this.normalizeLessonVocabTerm(t)}|${this.normalizeLessonVocabTerm(g)}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
@@ -2125,6 +2125,34 @@ export default {
|
||||
.replace(/^[.,!?;:]+|[.,!?;:]+$/g, '')
|
||||
.trim();
|
||||
},
|
||||
lessonVocabWordCount(value) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/[\p{P}\p{S}]+/gu, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.length;
|
||||
},
|
||||
looksLikeLessonVocabFragmentMismatch(left, right) {
|
||||
const leftWords = this.lessonVocabWordCount(left);
|
||||
const rightWords = this.lessonVocabWordCount(right);
|
||||
const leftText = String(left || '').trim();
|
||||
const rightText = String(right || '').trim();
|
||||
const leftLooksSentence = leftWords >= 3 || /[?!.].+\S/.test(leftText);
|
||||
const rightLooksSentence = rightWords >= 3 || /[?!.].+\S/.test(rightText);
|
||||
const leftLooksShortFragment = leftWords <= 1 && leftText.length <= 12;
|
||||
const rightLooksShortFragment = rightWords <= 1 && rightText.length <= 12;
|
||||
|
||||
return (leftLooksShortFragment && rightLooksSentence) || (rightLooksShortFragment && leftLooksSentence);
|
||||
},
|
||||
isTrainableLessonVocabPair(learning, reference) {
|
||||
const l = String(learning || '').trim();
|
||||
const r = String(reference || '').trim();
|
||||
if (!l || !r || this.normalizeLessonVocabTerm(l) === this.normalizeLessonVocabTerm(r)) {
|
||||
return false;
|
||||
}
|
||||
return !this.looksLikeLessonVocabFragmentMismatch(l, r);
|
||||
},
|
||||
normalizeCorePatternEntry(p) {
|
||||
if (p && typeof p === 'object' && p.target) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user