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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._looksLikeFragmentMismatch(learning, reference)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return !this._isInstructionLikeText(learning) && !this._isInstructionLikeText(reference);
|
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 } = {}) {
|
_calculateSrsSchedule(item, { correct, rating = null } = {}) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const previousStage = Math.max(0, Number(item?.stage) || 0);
|
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-if="lastCorrect">{{ $t('socialnetwork.vocab.practice.correct') }}</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
{{ $t('socialnetwork.vocab.practice.wrong') }}
|
{{ $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>
|
</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 v-if="showSrsRatingButtons" class="srs-rating">
|
||||||
<div class="srs-rating__title">{{ $t('socialnetwork.vocab.practice.srsRateTitle') }}</div>
|
<div class="srs-rating__title">{{ $t('socialnetwork.vocab.practice.srsRateTitle') }}</div>
|
||||||
<button
|
<button
|
||||||
@@ -169,6 +174,7 @@ export default {
|
|||||||
current: null, // { id, learning, reference }
|
current: null, // { id, learning, reference }
|
||||||
direction: 'L2R', // L2R: learning->reference, R2L: reference->learning
|
direction: 'L2R', // L2R: learning->reference, R2L: reference->learning
|
||||||
acceptableAnswers: [],
|
acceptableAnswers: [],
|
||||||
|
submittedAnswer: '',
|
||||||
choiceOptions: [],
|
choiceOptions: [],
|
||||||
typedAnswer: '',
|
typedAnswer: '',
|
||||||
answered: false,
|
answered: false,
|
||||||
@@ -211,6 +217,17 @@ export default {
|
|||||||
showSrsRatingButtons() {
|
showSrsRatingButtons() {
|
||||||
return this.srsMode && this.answered && !this.locked;
|
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() {
|
srsRatingOptions() {
|
||||||
if (!this.answered) {
|
if (!this.answered) {
|
||||||
return [];
|
return [];
|
||||||
@@ -429,8 +446,31 @@ export default {
|
|||||||
if (!learning || !reference || this.normalize(learning) === this.normalize(reference)) {
|
if (!learning || !reference || this.normalize(learning) === this.normalize(reference)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (this.looksLikeFragmentMismatch(learning, reference)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return !this.isInstructionLikeText(learning) && !this.isInstructionLikeText(reference);
|
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 = []) {
|
normalizePool(items = []) {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
return (Array.isArray(items) ? items : [])
|
return (Array.isArray(items) ? items : [])
|
||||||
@@ -458,6 +498,7 @@ export default {
|
|||||||
this.current = null;
|
this.current = null;
|
||||||
this.direction = this.openParams?.lessonId ? 'L2R' : (Math.random() < 0.5 ? 'L2R' : 'R2L');
|
this.direction = this.openParams?.lessonId ? 'L2R' : (Math.random() < 0.5 ? 'L2R' : 'R2L');
|
||||||
this.acceptableAnswers = [];
|
this.acceptableAnswers = [];
|
||||||
|
this.submittedAnswer = '';
|
||||||
this.choiceOptions = [];
|
this.choiceOptions = [];
|
||||||
this.typedAnswer = '';
|
this.typedAnswer = '';
|
||||||
this.answered = false;
|
this.answered = false;
|
||||||
@@ -473,6 +514,7 @@ export default {
|
|||||||
this.answered = false;
|
this.answered = false;
|
||||||
this.lastCorrect = false;
|
this.lastCorrect = false;
|
||||||
this.typedAnswer = '';
|
this.typedAnswer = '';
|
||||||
|
this.submittedAnswer = '';
|
||||||
|
|
||||||
if (!this.pool || this.pool.length === 0) return;
|
if (!this.pool || this.pool.length === 0) return;
|
||||||
|
|
||||||
@@ -746,6 +788,7 @@ export default {
|
|||||||
},
|
},
|
||||||
submitChoice(opt) {
|
submitChoice(opt) {
|
||||||
if (this.locked) return;
|
if (this.locked) return;
|
||||||
|
this.submittedAnswer = String(opt || '').trim();
|
||||||
const ok = this.acceptableAnswers.map(this.normalize).includes(this.normalize(opt));
|
const ok = this.acceptableAnswers.map(this.normalize).includes(this.normalize(opt));
|
||||||
this.markResult(ok);
|
this.markResult(ok);
|
||||||
this.playSound(ok);
|
this.playSound(ok);
|
||||||
@@ -760,6 +803,7 @@ export default {
|
|||||||
},
|
},
|
||||||
submitTyped() {
|
submitTyped() {
|
||||||
if (this.locked) return;
|
if (this.locked) return;
|
||||||
|
this.submittedAnswer = String(this.typedAnswer || '').trim();
|
||||||
const ans = this.normalize(this.typedAnswer);
|
const ans = this.normalize(this.typedAnswer);
|
||||||
const ok = this.acceptableAnswers.map(this.normalize).includes(ans);
|
const ok = this.acceptableAnswers.map(this.normalize).includes(ans);
|
||||||
this.markResult(ok);
|
this.markResult(ok);
|
||||||
@@ -927,6 +971,25 @@ export default {
|
|||||||
background: #ffecec;
|
background: #ffecec;
|
||||||
border-color: #d33;
|
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 {
|
.answersTitle {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
@@ -451,6 +451,8 @@
|
|||||||
"correct": "Richtig!",
|
"correct": "Richtig!",
|
||||||
"wrong": "Falsch.",
|
"wrong": "Falsch.",
|
||||||
"acceptable": "Mögliche richtige Übersetzungen:",
|
"acceptable": "Mögliche richtige Übersetzungen:",
|
||||||
|
"correctSolutions": "Richtige Antwort:",
|
||||||
|
"yourAnswer": "Deine Antwort:",
|
||||||
"stats": "Statistik",
|
"stats": "Statistik",
|
||||||
"dueToday": "Heute fällig",
|
"dueToday": "Heute fällig",
|
||||||
"done": "Erledigt",
|
"done": "Erledigt",
|
||||||
|
|||||||
@@ -451,6 +451,8 @@
|
|||||||
"correct": "Correct!",
|
"correct": "Correct!",
|
||||||
"wrong": "Wrong.",
|
"wrong": "Wrong.",
|
||||||
"acceptable": "Acceptable answers:",
|
"acceptable": "Acceptable answers:",
|
||||||
|
"correctSolutions": "Correct answer:",
|
||||||
|
"yourAnswer": "Your answer:",
|
||||||
"stats": "Stats",
|
"stats": "Stats",
|
||||||
"dueToday": "Due today",
|
"dueToday": "Due today",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
|
|||||||
@@ -1452,7 +1452,7 @@ export default {
|
|||||||
const oriented = orientPair(entry?.learning, entry?.reference);
|
const oriented = orientPair(entry?.learning, entry?.reference);
|
||||||
const reference = String(oriented.reference || '').trim();
|
const reference = String(oriented.reference || '').trim();
|
||||||
const learning = String(oriented.learning || '').trim();
|
const learning = String(oriented.learning || '').trim();
|
||||||
if (!reference) return;
|
if (!this.isTrainableLessonVocabPair(learning, reference)) return;
|
||||||
const key = this.normalizeLessonVocabTerm(reference);
|
const key = this.normalizeLessonVocabTerm(reference);
|
||||||
if (!vocabByReference.has(key)) {
|
if (!vocabByReference.has(key)) {
|
||||||
const variants = new Set();
|
const variants = new Set();
|
||||||
@@ -1523,7 +1523,7 @@ export default {
|
|||||||
return { from, to, page, pages: totalPages, total: n };
|
return { from, to, page, pages: totalPages, total: n };
|
||||||
},
|
},
|
||||||
trainableLessonVocab() {
|
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() {
|
lessonDidactics() {
|
||||||
return this.lesson?.didactics || {
|
return this.lesson?.didactics || {
|
||||||
@@ -1610,7 +1610,7 @@ export default {
|
|||||||
const oriented = orientPair(target, gloss);
|
const oriented = orientPair(target, gloss);
|
||||||
const t = String(oriented.target || '').trim();
|
const t = String(oriented.target || '').trim();
|
||||||
const g = String(oriented.gloss || '').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)}`;
|
const key = `${this.normalizeLessonVocabTerm(t)}|${this.normalizeLessonVocabTerm(g)}`;
|
||||||
if (seen.has(key)) return;
|
if (seen.has(key)) return;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
@@ -2125,6 +2125,34 @@ export default {
|
|||||||
.replace(/^[.,!?;:]+|[.,!?;:]+$/g, '')
|
.replace(/^[.,!?;:]+|[.,!?;:]+$/g, '')
|
||||||
.trim();
|
.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) {
|
normalizeCorePatternEntry(p) {
|
||||||
if (p && typeof p === 'object' && p.target) {
|
if (p && typeof p === 'object' && p.target) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user