feat(DayProduction, FalukantService, VocabLessonView): enhance vocabulary training and production tracking
All checks were successful
Deploy to production / deploy (push) Successful in 1m51s
All checks were successful
Deploy to production / deploy (push) Successful in 1m51s
- Added a new `completionCount` field to the DayProduction model to track the number of completed productions. - Updated the FalukantService to aggregate completed productions using the new `completionCount` field, improving accuracy in production statistics. - Introduced new vocabulary training features in VocabLessonView, including options to mark vocabulary as difficult and track remaining hard vocabulary, enhancing user engagement and learning effectiveness. - Updated localization files for German and English to support new vocabulary training features, ensuring a consistent user experience across languages.
This commit is contained in:
@@ -23,7 +23,11 @@ DayProduction.init({
|
|||||||
productionDate: {
|
productionDate: {
|
||||||
type: DataTypes.DATEONLY,
|
type: DataTypes.DATEONLY,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: sequelize.literal('CURRENT_DATE')}
|
defaultValue: sequelize.literal('CURRENT_DATE')},
|
||||||
|
completionCount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 1}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'DayProduction',
|
modelName: 'DayProduction',
|
||||||
|
|||||||
@@ -2968,7 +2968,9 @@ class FalukantService extends BaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Zertifikat: abgeschlossene Produktionen über alle Regionen/Niederlassungen.
|
* Zertifikat: abgeschlossene Produktionen über alle Regionen/Niederlassungen.
|
||||||
* Es zählt jede abgeschlossene Produktion (ein Datensatz in falukant_log.production).
|
* Es zählt jede abgeschlossene Produktion.
|
||||||
|
* Seit Daemon-Migration über falukant_log.production.completion_count
|
||||||
|
* (aggregierte Zeilen => SUM(completion_count), nicht COUNT(*)).
|
||||||
* Filter bei gesetztem countSince wie Daemon (GET_PRODUCTION_CERTIFICATE_INPUT_ROWS):
|
* Filter bei gesetztem countSince wie Daemon (GET_PRODUCTION_CERTIFICATE_INPUT_ROWS):
|
||||||
* COALESCE(production_timestamp, production_date::timestamp) >= countSince.
|
* COALESCE(production_timestamp, production_date::timestamp) >= countSince.
|
||||||
*
|
*
|
||||||
@@ -2983,7 +2985,7 @@ class FalukantService extends BaseService {
|
|||||||
if (countSince) replacements.countSince = countSince;
|
if (countSince) replacements.countSince = countSince;
|
||||||
const rows = await sequelize.query(
|
const rows = await sequelize.query(
|
||||||
`
|
`
|
||||||
SELECT COUNT(*)::int AS cnt
|
SELECT COALESCE(SUM(COALESCE(completion_count, 1)), 0)::int AS cnt
|
||||||
FROM falukant_log.production
|
FROM falukant_log.production
|
||||||
WHERE producer_id = :producerId${sinceClause}
|
WHERE producer_id = :producerId${sinceClause}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -825,6 +825,13 @@
|
|||||||
"trainerProgressNewContent": "Neue Inhalte: {current}/{target}",
|
"trainerProgressNewContent": "Neue Inhalte: {current}/{target}",
|
||||||
"trainerProgressReview": "Wiederholung: {count}",
|
"trainerProgressReview": "Wiederholung: {count}",
|
||||||
"trainerProgressMixShare": "Mischanteil: {percent}%",
|
"trainerProgressMixShare": "Mischanteil: {percent}%",
|
||||||
|
"markVocabHard": "Als schwer markieren",
|
||||||
|
"markVocabHardSaved": "Vokabel als schwer markiert.",
|
||||||
|
"unmarkVocabHard": "Aus Schwerliste entfernen",
|
||||||
|
"unmarkVocabHardSaved": "Vokabel aus der Schwerliste entfernt.",
|
||||||
|
"hardVocabModeActive": "Intensivblock: schwere Vokabeln",
|
||||||
|
"hardVocabRemaining": "Offen bis gefestigt: {count}",
|
||||||
|
"startHardVocabTrainer": "Schwere Vokabeln trainieren ({count})",
|
||||||
"unknownExerciseTypeNotice": "Dieser Übungstyp wird in der aktuellen Ansicht noch nicht interaktiv dargestellt.",
|
"unknownExerciseTypeNotice": "Dieser Übungstyp wird in der aktuellen Ansicht noch nicht interaktiv dargestellt.",
|
||||||
"unknownExerciseTypeLabel": "Typ: {type}",
|
"unknownExerciseTypeLabel": "Typ: {type}",
|
||||||
"lessonReviewHeadlineDone": "Diese Lektion ist in der freien Vertiefung angekommen.",
|
"lessonReviewHeadlineDone": "Diese Lektion ist in der freien Vertiefung angekommen.",
|
||||||
|
|||||||
@@ -825,6 +825,13 @@
|
|||||||
"trainerProgressNewContent": "New content: {current}/{target}",
|
"trainerProgressNewContent": "New content: {current}/{target}",
|
||||||
"trainerProgressReview": "Review: {count}",
|
"trainerProgressReview": "Review: {count}",
|
||||||
"trainerProgressMixShare": "Mixed share: {percent}%",
|
"trainerProgressMixShare": "Mixed share: {percent}%",
|
||||||
|
"markVocabHard": "Mark as difficult",
|
||||||
|
"markVocabHardSaved": "Vocabulary marked as difficult.",
|
||||||
|
"unmarkVocabHard": "Remove from difficult list",
|
||||||
|
"unmarkVocabHardSaved": "Vocabulary removed from difficult list.",
|
||||||
|
"hardVocabModeActive": "Intensive block: difficult vocabulary",
|
||||||
|
"hardVocabRemaining": "Remaining until stable: {count}",
|
||||||
|
"startHardVocabTrainer": "Train difficult vocabulary ({count})",
|
||||||
"unknownExerciseTypeNotice": "This exercise type is not displayed interactively in the current view yet.",
|
"unknownExerciseTypeNotice": "This exercise type is not displayed interactively in the current view yet.",
|
||||||
"unknownExerciseTypeLabel": "Type: {type}",
|
"unknownExerciseTypeLabel": "Type: {type}",
|
||||||
"lessonReviewHeadlineDone": "This lesson has reached the free practice stage.",
|
"lessonReviewHeadlineDone": "This lesson has reached the free practice stage.",
|
||||||
|
|||||||
@@ -235,6 +235,13 @@
|
|||||||
<button @click="startVocabTrainer" class="btn-start-trainer">
|
<button @click="startVocabTrainer" class="btn-start-trainer">
|
||||||
{{ hasPreviousVocab ? $t('socialnetwork.vocab.courses.startLesson') : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
|
{{ hasPreviousVocab ? $t('socialnetwork.vocab.courses.startLesson') : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="hasCrossChapterHardVocab"
|
||||||
|
@click="startHardVocabTrainer"
|
||||||
|
class="btn-start-trainer button-secondary"
|
||||||
|
>
|
||||||
|
{{ $t('socialnetwork.vocab.courses.startHardVocabTrainer', { count: crossChapterHardVocab.length }) }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<p v-else class="vocab-trainer-locked-hint">{{ $t('socialnetwork.vocab.courses.vocabTrainerLockedHint') }}</p>
|
<p v-else class="vocab-trainer-locked-hint">{{ $t('socialnetwork.vocab.courses.vocabTrainerLockedHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,6 +278,10 @@
|
|||||||
</span>
|
</span>
|
||||||
<button @click="stopVocabTrainer" class="btn-stop-trainer">{{ $t('socialnetwork.vocab.courses.stopTrainer') }}</button>
|
<button @click="stopVocabTrainer" class="btn-stop-trainer">{{ $t('socialnetwork.vocab.courses.stopTrainer') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="vocabTrainerHardMode" class="stats-row trainer-progress-row">
|
||||||
|
<span>{{ $t('socialnetwork.vocab.courses.hardVocabModeActive') }}</span>
|
||||||
|
<span>{{ $t('socialnetwork.vocab.courses.hardVocabRemaining', { count: hardVocabRemainingCount }) }}</span>
|
||||||
|
</div>
|
||||||
<div v-if="hasPreviousVocab" class="stats-row trainer-progress-row">
|
<div v-if="hasPreviousVocab" class="stats-row trainer-progress-row">
|
||||||
<span>{{ $t('socialnetwork.vocab.courses.trainerProgressNewContent', { current: vocabTrainerCurrentAttempts, target: trainerNewFocusTarget }) }}</span>
|
<span>{{ $t('socialnetwork.vocab.courses.trainerProgressNewContent', { current: vocabTrainerCurrentAttempts, target: trainerNewFocusTarget }) }}</span>
|
||||||
<span>{{ $t('socialnetwork.vocab.courses.trainerProgressReview', { count: vocabTrainerReviewAttempts }) }}</span>
|
<span>{{ $t('socialnetwork.vocab.courses.trainerProgressReview', { count: vocabTrainerReviewAttempts }) }}</span>
|
||||||
@@ -288,6 +299,19 @@
|
|||||||
{{ $t('socialnetwork.vocab.courses.wrong') }}. {{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ currentVocabQuestion.answer }}
|
{{ $t('socialnetwork.vocab.courses.wrong') }}. {{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ currentVocabQuestion.answer }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="vocab-hard-actions">
|
||||||
|
<button type="button" class="btn-switch-mode" @click="markCurrentVocabAsHard">
|
||||||
|
{{ $t('socialnetwork.vocab.courses.markVocabHard') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isCurrentVocabMarkedHard"
|
||||||
|
type="button"
|
||||||
|
class="btn-switch-mode"
|
||||||
|
@click="unmarkCurrentVocabAsHard"
|
||||||
|
>
|
||||||
|
{{ $t('socialnetwork.vocab.courses.unmarkVocabHard') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<!-- Multiple Choice Modus -->
|
<!-- Multiple Choice Modus -->
|
||||||
<div v-if="vocabTrainerMode === 'multiple_choice' && !vocabTrainerAnswered" class="vocab-answer-area multiple-choice">
|
<div v-if="vocabTrainerMode === 'multiple_choice' && !vocabTrainerAnswered" class="vocab-answer-area multiple-choice">
|
||||||
<div class="choice-buttons">
|
<div class="choice-buttons">
|
||||||
@@ -1104,6 +1128,11 @@ export default {
|
|||||||
vocabTrainerContinueTimer: null,
|
vocabTrainerContinueTimer: null,
|
||||||
vocabTrainerLastWrongReview: null,
|
vocabTrainerLastWrongReview: null,
|
||||||
vocabTrainerDirection: 'L2R', // L2R: learning->reference, R2L: reference->learning
|
vocabTrainerDirection: 'L2R', // L2R: learning->reference, R2L: reference->learning
|
||||||
|
vocabTrainerSessionType: 'lesson', // 'lesson' | 'hard_collection'
|
||||||
|
vocabTrainerHardPool: [],
|
||||||
|
vocabTrainerHardMode: false,
|
||||||
|
vocabTrainerHardMastery: {}, // { [vocabKey]: consecutiveCorrectInHardMode }
|
||||||
|
manualHardVocabMap: {}, // persistente manuell markierte schwere Vokabeln je Lektion
|
||||||
isCheckingLessonCompletion: false, // Flag um Endlosschleife zu verhindern
|
isCheckingLessonCompletion: false, // Flag um Endlosschleife zu verhindern
|
||||||
isNavigatingToNext: false, // Flag um mehrfache Navigation zu verhindern
|
isNavigatingToNext: false, // Flag um mehrfache Navigation zu verhindern
|
||||||
showNextLessonDialog: false,
|
showNextLessonDialog: false,
|
||||||
@@ -1258,6 +1287,45 @@ export default {
|
|||||||
}
|
}
|
||||||
return 0.5;
|
return 0.5;
|
||||||
},
|
},
|
||||||
|
crossChapterHardVocab() {
|
||||||
|
const map = new Map();
|
||||||
|
const pushHard = (entry) => {
|
||||||
|
const learning = String(entry?.learning || '').trim();
|
||||||
|
const reference = String(entry?.reference || '').trim();
|
||||||
|
if (!this.isTrainableLessonVocabPair(learning, reference)) return;
|
||||||
|
const key = `${learning}|${reference}`;
|
||||||
|
const prev = map.get(key) || { learning, reference, wrongCount: 0 };
|
||||||
|
prev.wrongCount += Math.max(1, Number(entry?.wrongCount) || 1);
|
||||||
|
map.set(key, prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
(this.courseProgressList || []).forEach((entry) => {
|
||||||
|
const weak = Array.isArray(entry?.lessonState?.reviewWeakVocab) ? entry.lessonState.reviewWeakVocab : [];
|
||||||
|
const manual = Array.isArray(entry?.lessonState?.manualHardVocab) ? entry.lessonState.manualHardVocab : [];
|
||||||
|
weak.forEach(pushHard);
|
||||||
|
manual.forEach(pushHard);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(map.values())
|
||||||
|
.sort((a, b) => b.wrongCount - a.wrongCount)
|
||||||
|
.slice(0, 80)
|
||||||
|
.map((v) => ({ learning: v.learning, reference: v.reference }));
|
||||||
|
},
|
||||||
|
hasCrossChapterHardVocab() {
|
||||||
|
return this.crossChapterHardVocab.length > 0;
|
||||||
|
},
|
||||||
|
hardVocabRemainingCount() {
|
||||||
|
const requiredConsecutiveCorrect = 2;
|
||||||
|
return this.vocabTrainerHardPool.filter((vocab) => {
|
||||||
|
const key = this.getVocabKey(vocab);
|
||||||
|
return (Number(this.vocabTrainerHardMastery[key]) || 0) < requiredConsecutiveCorrect;
|
||||||
|
}).length;
|
||||||
|
},
|
||||||
|
isCurrentVocabMarkedHard() {
|
||||||
|
const key = this.currentVocabQuestion?.key || '';
|
||||||
|
if (!key) return false;
|
||||||
|
return Boolean(this.manualHardVocabMap[key]);
|
||||||
|
},
|
||||||
canAccessExercises() {
|
canAccessExercises() {
|
||||||
if (!this.hasExercises) return false;
|
if (!this.hasExercises) return false;
|
||||||
if (this.exerciseNeedsReinforcement) return false;
|
if (this.exerciseNeedsReinforcement) return false;
|
||||||
@@ -1791,6 +1859,11 @@ export default {
|
|||||||
vocabTrainerRepeatQueue: this.vocabTrainerRepeatQueue,
|
vocabTrainerRepeatQueue: this.vocabTrainerRepeatQueue,
|
||||||
vocabTrainerCurrentAttempts: this.vocabTrainerCurrentAttempts,
|
vocabTrainerCurrentAttempts: this.vocabTrainerCurrentAttempts,
|
||||||
vocabTrainerReviewAttempts: this.vocabTrainerReviewAttempts,
|
vocabTrainerReviewAttempts: this.vocabTrainerReviewAttempts,
|
||||||
|
vocabTrainerSessionType: this.vocabTrainerSessionType,
|
||||||
|
vocabTrainerHardPool: this.vocabTrainerHardPool,
|
||||||
|
vocabTrainerHardMode: this.vocabTrainerHardMode,
|
||||||
|
vocabTrainerHardMastery: this.vocabTrainerHardMastery,
|
||||||
|
manualHardVocab: Object.values(this.manualHardVocabMap),
|
||||||
exerciseRetryPending: this.exerciseRetryPending,
|
exerciseRetryPending: this.exerciseRetryPending,
|
||||||
exerciseRetryPendingSinceAttempts: this.exerciseRetryPendingSinceAttempts,
|
exerciseRetryPendingSinceAttempts: this.exerciseRetryPendingSinceAttempts,
|
||||||
exerciseSequentialIndex: this.exerciseSequentialIndex
|
exerciseSequentialIndex: this.exerciseSequentialIndex
|
||||||
@@ -2104,6 +2177,26 @@ export default {
|
|||||||
: {};
|
: {};
|
||||||
this.vocabTrainerCurrentAttempts = Math.max(0, Number(parsedState.vocabTrainerCurrentAttempts) || 0);
|
this.vocabTrainerCurrentAttempts = Math.max(0, Number(parsedState.vocabTrainerCurrentAttempts) || 0);
|
||||||
this.vocabTrainerReviewAttempts = Math.max(0, Number(parsedState.vocabTrainerReviewAttempts) || 0);
|
this.vocabTrainerReviewAttempts = Math.max(0, Number(parsedState.vocabTrainerReviewAttempts) || 0);
|
||||||
|
this.vocabTrainerSessionType = parsedState.vocabTrainerSessionType === 'hard_collection' ? 'hard_collection' : 'lesson';
|
||||||
|
this.vocabTrainerHardPool = Array.isArray(parsedState.vocabTrainerHardPool) ? parsedState.vocabTrainerHardPool : [];
|
||||||
|
this.vocabTrainerHardMode = Boolean(parsedState.vocabTrainerHardMode);
|
||||||
|
this.vocabTrainerHardMastery = parsedState.vocabTrainerHardMastery && typeof parsedState.vocabTrainerHardMastery === 'object'
|
||||||
|
? parsedState.vocabTrainerHardMastery
|
||||||
|
: {};
|
||||||
|
const savedManualHard = Array.isArray(parsedState.manualHardVocab) ? parsedState.manualHardVocab : [];
|
||||||
|
this.manualHardVocabMap = {};
|
||||||
|
savedManualHard.forEach((entry) => {
|
||||||
|
const learning = String(entry?.learning || '').trim();
|
||||||
|
const reference = String(entry?.reference || '').trim();
|
||||||
|
if (!this.isTrainableLessonVocabPair(learning, reference)) return;
|
||||||
|
const key = `${learning}|${reference}`;
|
||||||
|
this.manualHardVocabMap[key] = {
|
||||||
|
learning,
|
||||||
|
reference,
|
||||||
|
wrongCount: Math.max(1, Number(entry?.wrongCount) || 1),
|
||||||
|
lastWrongAt: String(entry?.lastWrongAt || new Date().toISOString())
|
||||||
|
};
|
||||||
|
});
|
||||||
this.exerciseRetryPending = Boolean(parsedState.exerciseRetryPending);
|
this.exerciseRetryPending = Boolean(parsedState.exerciseRetryPending);
|
||||||
this.exerciseRetryPendingSinceAttempts = Math.max(0, Number(parsedState.exerciseRetryPendingSinceAttempts) || 0);
|
this.exerciseRetryPendingSinceAttempts = Math.max(0, Number(parsedState.exerciseRetryPendingSinceAttempts) || 0);
|
||||||
const maxIdx = Math.max(0, this.scrambledChapterExamExercises.length - 1);
|
const maxIdx = Math.max(0, this.scrambledChapterExamExercises.length - 1);
|
||||||
@@ -2657,6 +2750,11 @@ export default {
|
|||||||
this.vocabTrainerPool = [];
|
this.vocabTrainerPool = [];
|
||||||
this.vocabTrainerMixedPool = [];
|
this.vocabTrainerMixedPool = [];
|
||||||
this.vocabTrainerRepeatQueue = [];
|
this.vocabTrainerRepeatQueue = [];
|
||||||
|
this.vocabTrainerHardPool = [];
|
||||||
|
this.vocabTrainerHardMode = false;
|
||||||
|
this.vocabTrainerHardMastery = {};
|
||||||
|
this.manualHardVocabMap = {};
|
||||||
|
this.vocabTrainerSessionType = 'lesson';
|
||||||
this.vocabTrainerPhase = 'current';
|
this.vocabTrainerPhase = 'current';
|
||||||
this.vocabTrainerCurrentAttempts = 0;
|
this.vocabTrainerCurrentAttempts = 0;
|
||||||
this.vocabTrainerReviewAttempts = 0;
|
this.vocabTrainerReviewAttempts = 0;
|
||||||
@@ -3370,6 +3468,10 @@ export default {
|
|||||||
this.vocabTrainerStats = {};
|
this.vocabTrainerStats = {};
|
||||||
this.vocabTrainerRepeatQueue = [];
|
this.vocabTrainerRepeatQueue = [];
|
||||||
this.vocabTrainerLastWrongReview = null;
|
this.vocabTrainerLastWrongReview = null;
|
||||||
|
this.vocabTrainerSessionType = 'lesson';
|
||||||
|
this.vocabTrainerHardPool = [];
|
||||||
|
this.vocabTrainerHardMode = false;
|
||||||
|
this.vocabTrainerHardMastery = {};
|
||||||
// Bereite Mixed-Pool aus alten Vokabeln vor (ohne Duplikate aus aktueller Lektion)
|
// Bereite Mixed-Pool aus alten Vokabeln vor (ohne Duplikate aus aktueller Lektion)
|
||||||
this.vocabTrainerMixedPool = this._buildMixedPool();
|
this.vocabTrainerMixedPool = this._buildMixedPool();
|
||||||
this.vocabTrainerPhase = 'current';
|
this.vocabTrainerPhase = 'current';
|
||||||
@@ -3392,12 +3494,41 @@ export default {
|
|||||||
this.vocabTrainerReviewAttempts = 0;
|
this.vocabTrainerReviewAttempts = 0;
|
||||||
this.vocabTrainerMixedPool = [];
|
this.vocabTrainerMixedPool = [];
|
||||||
this.vocabTrainerRepeatQueue = [];
|
this.vocabTrainerRepeatQueue = [];
|
||||||
|
this.vocabTrainerHardPool = [];
|
||||||
|
this.vocabTrainerHardMode = false;
|
||||||
|
this.vocabTrainerHardMastery = {};
|
||||||
|
this.vocabTrainerSessionType = 'lesson';
|
||||||
this.currentVocabQuestion = null;
|
this.currentVocabQuestion = null;
|
||||||
this.vocabTrainerAnswer = '';
|
this.vocabTrainerAnswer = '';
|
||||||
this.vocabTrainerSelectedChoice = null;
|
this.vocabTrainerSelectedChoice = null;
|
||||||
this.vocabTrainerAnswered = false;
|
this.vocabTrainerAnswered = false;
|
||||||
this.vocabTrainerLastWrongReview = null;
|
this.vocabTrainerLastWrongReview = null;
|
||||||
},
|
},
|
||||||
|
startHardVocabTrainer() {
|
||||||
|
const hardPool = this.crossChapterHardVocab.slice();
|
||||||
|
if (!hardPool.length) return;
|
||||||
|
this.vocabTrainerActive = true;
|
||||||
|
this.vocabTrainerMode = 'multiple_choice';
|
||||||
|
this.vocabTrainerAutoSwitchedToTyping = false;
|
||||||
|
this.vocabTrainerCorrect = 0;
|
||||||
|
this.vocabTrainerWrong = 0;
|
||||||
|
this.vocabTrainerTotalAttempts = 0;
|
||||||
|
this.vocabTrainerCurrentAttempts = 0;
|
||||||
|
this.vocabTrainerReviewAttempts = 0;
|
||||||
|
this.vocabTrainerStats = {};
|
||||||
|
this.vocabTrainerRepeatQueue = [];
|
||||||
|
this.vocabTrainerSessionType = 'hard_collection';
|
||||||
|
this.vocabTrainerHardPool = hardPool;
|
||||||
|
this.vocabTrainerHardMode = true;
|
||||||
|
this.vocabTrainerHardMastery = {};
|
||||||
|
this.vocabTrainerMixedPool = [];
|
||||||
|
this.vocabTrainerPool = hardPool.slice();
|
||||||
|
this.currentVocabQuestion = null;
|
||||||
|
this.vocabTrainerLastWrongReview = null;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.nextVocabQuestion();
|
||||||
|
});
|
||||||
|
},
|
||||||
/** Erstellt den Mixed-Pool aus vorherigen Lektions-Vokabeln (ohne Duplikate der aktuellen Lektion) */
|
/** Erstellt den Mixed-Pool aus vorherigen Lektions-Vokabeln (ohne Duplikate der aktuellen Lektion) */
|
||||||
_buildMixedPool() {
|
_buildMixedPool() {
|
||||||
if (!this.previousVocab || this.previousVocab.length === 0) return [];
|
if (!this.previousVocab || this.previousVocab.length === 0) return [];
|
||||||
@@ -3444,6 +3575,50 @@ export default {
|
|||||||
getVocabKey(vocab) {
|
getVocabKey(vocab) {
|
||||||
return `${vocab.learning}|${vocab.reference}`;
|
return `${vocab.learning}|${vocab.reference}`;
|
||||||
},
|
},
|
||||||
|
markCurrentVocabAsHard() {
|
||||||
|
if (!this.currentVocabQuestion?.vocab) return;
|
||||||
|
const vocab = this.currentVocabQuestion.vocab;
|
||||||
|
const key = this.getVocabKey(vocab);
|
||||||
|
this.manualHardVocabMap[key] = {
|
||||||
|
learning: String(vocab.learning || '').trim(),
|
||||||
|
reference: String(vocab.reference || '').trim(),
|
||||||
|
wrongCount: Math.max(1, Number(this.manualHardVocabMap[key]?.wrongCount) || 1),
|
||||||
|
lastWrongAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
if (!this.vocabTrainerHardPool.some((entry) => this.getVocabKey(entry) === key)) {
|
||||||
|
this.vocabTrainerHardPool.push(vocab);
|
||||||
|
}
|
||||||
|
if (this.vocabTrainerHardMastery[key] == null) {
|
||||||
|
this.vocabTrainerHardMastery[key] = 0;
|
||||||
|
}
|
||||||
|
this.$root?.$refs?.messageDialog?.open?.('tr:socialnetwork.vocab.courses.markVocabHardSaved');
|
||||||
|
},
|
||||||
|
unmarkCurrentVocabAsHard() {
|
||||||
|
if (!this.currentVocabQuestion?.key) return;
|
||||||
|
const key = this.currentVocabQuestion.key;
|
||||||
|
delete this.manualHardVocabMap[key];
|
||||||
|
this.vocabTrainerHardPool = this.vocabTrainerHardPool.filter((entry) => this.getVocabKey(entry) !== key);
|
||||||
|
delete this.vocabTrainerHardMastery[key];
|
||||||
|
this.$root?.$refs?.messageDialog?.open?.('tr:socialnetwork.vocab.courses.unmarkVocabHardSaved');
|
||||||
|
this.maybeExitHardIntensivePhase();
|
||||||
|
},
|
||||||
|
maybeEnterHardIntensivePhase() {
|
||||||
|
if (this.vocabTrainerSessionType !== 'lesson') return;
|
||||||
|
if (this.vocabTrainerHardMode) return;
|
||||||
|
if (!this.vocabTrainerHardPool.length) return;
|
||||||
|
if (this.vocabTrainerCurrentAttempts < this.trainerNewFocusTarget) return;
|
||||||
|
this.vocabTrainerHardMode = true;
|
||||||
|
},
|
||||||
|
maybeExitHardIntensivePhase() {
|
||||||
|
if (!this.vocabTrainerHardMode) return;
|
||||||
|
if (!this.vocabTrainerHardPool.length) {
|
||||||
|
this.vocabTrainerHardMode = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.hardVocabRemainingCount <= 0) {
|
||||||
|
this.vocabTrainerHardMode = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
getVocabStats(vocab) {
|
getVocabStats(vocab) {
|
||||||
const key = this.getVocabKey(vocab);
|
const key = this.getVocabKey(vocab);
|
||||||
if (!this.vocabTrainerStats[key]) {
|
if (!this.vocabTrainerStats[key]) {
|
||||||
@@ -3770,6 +3945,68 @@ export default {
|
|||||||
|
|
||||||
// Prüfe ob Modus-Wechsel nötig ist
|
// Prüfe ob Modus-Wechsel nötig ist
|
||||||
this.checkVocabModeSwitch();
|
this.checkVocabModeSwitch();
|
||||||
|
this.maybeEnterHardIntensivePhase();
|
||||||
|
this.maybeExitHardIntensivePhase();
|
||||||
|
|
||||||
|
if (this.vocabTrainerSessionType === 'hard_collection' || this.vocabTrainerHardMode) {
|
||||||
|
const requiredConsecutiveCorrect = 2;
|
||||||
|
const pool = this.vocabTrainerHardPool.filter((vocab) => {
|
||||||
|
const key = this.getVocabKey(vocab);
|
||||||
|
return (Number(this.vocabTrainerHardMastery[key]) || 0) < requiredConsecutiveCorrect;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pool.length) {
|
||||||
|
this.vocabTrainerHardMode = false;
|
||||||
|
if (this.vocabTrainerSessionType === 'hard_collection') {
|
||||||
|
this.currentVocabQuestion = null;
|
||||||
|
this.vocabTrainerActive = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const vocab = this.chooseVocabFromPool(pool);
|
||||||
|
if (!vocab) {
|
||||||
|
this.currentVocabQuestion = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.vocabTrainerDirection = Math.random() < 0.5 ? 'L2R' : 'R2L';
|
||||||
|
const allTrainerVocabs = pool;
|
||||||
|
const direction = this.vocabTrainerDirection;
|
||||||
|
const prompt = direction === 'L2R' ? vocab.learning : vocab.reference;
|
||||||
|
const acceptableAnswers = this.getEquivalentVocabAnswers(prompt, direction, allTrainerVocabs);
|
||||||
|
this.currentVocabQuestion = {
|
||||||
|
vocab,
|
||||||
|
prompt,
|
||||||
|
answers: acceptableAnswers.length > 0
|
||||||
|
? acceptableAnswers
|
||||||
|
: [direction === 'L2R' ? vocab.reference : vocab.learning],
|
||||||
|
answer: acceptableAnswers.length > 0
|
||||||
|
? acceptableAnswers.join(' / ')
|
||||||
|
: (direction === 'L2R' ? vocab.reference : vocab.learning),
|
||||||
|
key: this.getVocabKey(vocab),
|
||||||
|
source: 'hard'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.vocabTrainerAnswer = '';
|
||||||
|
this.vocabTrainerSelectedChoice = null;
|
||||||
|
this.vocabTrainerAnswered = false;
|
||||||
|
|
||||||
|
if (this.vocabTrainerMode === 'multiple_choice') {
|
||||||
|
this.vocabTrainerChoiceOptions = this.buildChoiceOptions(
|
||||||
|
this.currentVocabQuestion.answers,
|
||||||
|
allTrainerVocabs,
|
||||||
|
this.currentVocabQuestion.prompt,
|
||||||
|
this.vocabTrainerDirection
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.vocabTrainerMode === 'typing') {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.vocabInput?.focus?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let questionSource = 'current';
|
let questionSource = 'current';
|
||||||
let sourcePool = this.trainableLessonVocab;
|
let sourcePool = this.trainableLessonVocab;
|
||||||
@@ -3969,6 +4206,17 @@ export default {
|
|||||||
this.vocabTrainerCorrect++;
|
this.vocabTrainerCorrect++;
|
||||||
stats.correct++;
|
stats.correct++;
|
||||||
this.resolveRepeatedVocab(this.currentVocabQuestion.vocab);
|
this.resolveRepeatedVocab(this.currentVocabQuestion.vocab);
|
||||||
|
if (this.currentVocabQuestion.source === 'hard') {
|
||||||
|
const key = this.currentVocabQuestion.key;
|
||||||
|
const nextMastery = Math.max(0, Number(this.vocabTrainerHardMastery[key]) || 0) + 1;
|
||||||
|
this.vocabTrainerHardMastery[key] = nextMastery;
|
||||||
|
// Wenn eine manuell markierte schwierige Vokabel mehrfach korrekt sitzt,
|
||||||
|
// automatisch aus der manuellen Schwerliste entfernen.
|
||||||
|
if (nextMastery >= 2 && this.manualHardVocabMap[key]) {
|
||||||
|
delete this.manualHardVocabMap[key];
|
||||||
|
this.vocabTrainerHardPool = this.vocabTrainerHardPool.filter((entry) => this.getVocabKey(entry) !== key);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.vocabTrainerLastWrongReview = {
|
this.vocabTrainerLastWrongReview = {
|
||||||
prompt: this.currentVocabQuestion.prompt,
|
prompt: this.currentVocabQuestion.prompt,
|
||||||
@@ -3978,6 +4226,10 @@ export default {
|
|||||||
this.vocabTrainerWrong++;
|
this.vocabTrainerWrong++;
|
||||||
stats.wrong++;
|
stats.wrong++;
|
||||||
this.queueFailedVocab(this.currentVocabQuestion.vocab);
|
this.queueFailedVocab(this.currentVocabQuestion.vocab);
|
||||||
|
if (this.currentVocabQuestion.source === 'hard') {
|
||||||
|
const key = this.currentVocabQuestion.key;
|
||||||
|
this.vocabTrainerHardMastery[key] = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.reportSrsReviewForCurrentQuestion(this.vocabTrainerLastCorrect);
|
this.reportSrsReviewForCurrentQuestion(this.vocabTrainerLastCorrect);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user