feat(bisaya-course): refine phase 4 didactics and enhance course content generation
All checks were successful
Deploy to production / deploy (push) Successful in 5m19s
All checks were successful
Deploy to production / deploy (push) Successful in 5m19s
- Corrected grammatical errors and improved the phrasing in the BISAYA_PHASE4_DIDACTICS, ensuring clarity and accuracy in the learning materials. - Updated the course content generation script to include lessons from phase 5, enhancing the overall structure and flow of the course. - Introduced a new vocabulary course content synchronization process, improving the integration of vocabulary resources across different modules. - Enhanced the VocabService to dynamically adjust temperature settings based on the mode, optimizing response generation for different contexts. - Added new localized titles and vocabulary entries in multiple languages, enriching the learning experience for users.
This commit is contained in:
@@ -102,6 +102,22 @@
|
||||
<p>{{ $t('socialnetwork.vocab.courses.intensiveReviewIntro') }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showDailyEnoughBanner"
|
||||
class="lesson-enough-banner"
|
||||
>
|
||||
<strong>{{ $t('socialnetwork.vocab.courses.dailyEnoughTitle') }}</strong>
|
||||
<p>{{ $t('socialnetwork.vocab.courses.dailyEnoughBody') }}</p>
|
||||
<div class="lesson-enough-banner__actions">
|
||||
<button type="button" class="btn-secondary" @click="dismissDailyEnoughBanner">
|
||||
{{ $t('socialnetwork.vocab.courses.dailyEnoughStop') }}
|
||||
</button>
|
||||
<button type="button" class="btn-primary" @click="continueDailyEnoughBanner">
|
||||
{{ $t('socialnetwork.vocab.courses.dailyEnoughContinue') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="lesson-primary-flow surface-card">
|
||||
<div class="lesson-primary-flow__header">
|
||||
<span class="lesson-primary-flow__eyebrow">{{ $t('socialnetwork.vocab.courses.learningPathLabel') }}</span>
|
||||
@@ -1092,6 +1108,7 @@ export default {
|
||||
chapterExamFailedDetails: [],
|
||||
/** Index in scrambledChapterExamExercises bei Ein-Frage-Ansicht */
|
||||
exerciseSequentialIndex: 0,
|
||||
dailyEnoughBannerDismissed: false,
|
||||
/** Aus vorherigen Lektionen (MC-Optionen nach Fragentyp Ziel-/Muttersprache) */
|
||||
distractorPool: { target: [], native: [] },
|
||||
courseLanguageName: '',
|
||||
@@ -1438,12 +1455,21 @@ export default {
|
||||
if (!reference) return;
|
||||
const key = this.normalizeLessonVocabTerm(reference);
|
||||
if (!vocabByReference.has(key)) {
|
||||
vocabByReference.set(key, { learning, reference });
|
||||
const variants = new Set();
|
||||
if (learning) variants.add(learning);
|
||||
vocabByReference.set(key, { learning, reference, learningVariants: variants });
|
||||
return;
|
||||
}
|
||||
const existing = vocabByReference.get(key);
|
||||
if (!existing.learning && learning) {
|
||||
existing.learning = learning;
|
||||
if (learning) {
|
||||
if (!existing.learning) {
|
||||
existing.learning = learning;
|
||||
}
|
||||
if (existing.learningVariants instanceof Set) {
|
||||
existing.learningVariants.add(learning);
|
||||
} else {
|
||||
existing.learningVariants = new Set([existing.learning, learning].filter(Boolean));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1458,7 +1484,18 @@ export default {
|
||||
addEntry(item);
|
||||
});
|
||||
|
||||
return Array.from(vocabByReference.values());
|
||||
return Array.from(vocabByReference.values()).map((entry) => {
|
||||
const variants = entry.learningVariants instanceof Set
|
||||
? Array.from(entry.learningVariants)
|
||||
: (Array.isArray(entry.learningVariants) ? entry.learningVariants : []);
|
||||
const uniqueVariants = [...new Set([entry.learning, ...variants].filter(Boolean))];
|
||||
return {
|
||||
learning: entry.learning,
|
||||
reference: entry.reference,
|
||||
// Für den Trainer: alternative Übersetzungen als gleichwertig akzeptieren.
|
||||
learningVariants: uniqueVariants
|
||||
};
|
||||
});
|
||||
},
|
||||
vocabOverviewTotalPages() {
|
||||
const n = this.lessonVocab.length;
|
||||
@@ -1673,6 +1710,23 @@ export default {
|
||||
due: this.formatLessonReviewDue(progress.reviewNextDueAt)
|
||||
});
|
||||
},
|
||||
showDailyEnoughBanner() {
|
||||
if (this.dailyEnoughBannerDismissed) return false;
|
||||
const progress = this.lessonProgress;
|
||||
// Wenn eine Review-Welle fällig ist, ist "für heute genug" nicht korrekt.
|
||||
if (progress?.completed && progress?.reviewDue) return false;
|
||||
|
||||
if (progress?.completed) return true;
|
||||
|
||||
// Falls der Fortschritt noch nicht gespeichert ist: Übungen sind vollständig + bestanden.
|
||||
if (this.hasExercises && this.effectiveExercises.length > 0) {
|
||||
const allAnswered = this.exerciseAnsweredCount >= this.effectiveExercises.length;
|
||||
if (allAnswered && this.exerciseProgressPercent >= this.exerciseTargetScore) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
assistantAvailable() {
|
||||
if (!this.assistantSettings) {
|
||||
return false;
|
||||
@@ -1748,6 +1802,22 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dismissDailyEnoughBanner() {
|
||||
this.dailyEnoughBannerDismissed = true;
|
||||
},
|
||||
continueDailyEnoughBanner() {
|
||||
this.dailyEnoughBannerDismissed = true;
|
||||
if (!this.vocabTrainerActive && this.trainableLessonVocab.length > 0) {
|
||||
// Startet Trainer nur, wenn die Vorbereitung abgeschlossen ist.
|
||||
if (this.canStartVocabTrainerPrep) {
|
||||
this.startVocabTrainer();
|
||||
} else {
|
||||
this.vocabTrainerActive = true;
|
||||
this.vocabTrainerPool = [...this.trainableLessonVocab];
|
||||
this.$nextTick(() => this.nextVocabQuestion());
|
||||
}
|
||||
}
|
||||
},
|
||||
vocabOverviewGoPrev() {
|
||||
if (this.vocabOverviewPagerMeta.page > 1) {
|
||||
this.vocabOverviewPage -= 1;
|
||||
@@ -2419,6 +2489,22 @@ export default {
|
||||
|
||||
// Nur extrahieren, wenn Muttersprache-Hinweise (Klammern) vorhanden sind
|
||||
if (nativeWords.length > 0) {
|
||||
// Wenn möglich: rekonstruiere die vollständige Phrase um die Lücke herum.
|
||||
// Beispiel: "{gap} ka mubalik? (Bitte wiederholen)" + "Palihug" => "Palihug ka mubalik?"
|
||||
const stripHints = (s) => String(s || '')
|
||||
.replace(/\([^)]*\)/g, '')
|
||||
.replace(/\[[^\]]*\]/g, '')
|
||||
.replace(/([^)]*)/g, '');
|
||||
const stopChars = [',', '.', '|', ';', ':', '\n'];
|
||||
const lastStopIndex = (s) => Math.max(...stopChars.map((c) => s.lastIndexOf(c)));
|
||||
const firstStopIndex = (s) => {
|
||||
const indices = stopChars
|
||||
.map((c) => s.indexOf(c))
|
||||
.filter((idx) => idx >= 0);
|
||||
return indices.length ? Math.min(...indices) : -1;
|
||||
};
|
||||
const parts = String(text).split('{gap}');
|
||||
|
||||
answers.forEach((answer, index) => {
|
||||
if (answer && answer.trim()) {
|
||||
const nativeWord = nativeWords[index];
|
||||
@@ -2429,13 +2515,38 @@ export default {
|
||||
const nativeLooksSentence = /[?.!]/.test(nativeText) || nativeWordCount >= 4;
|
||||
const answerLooksShortToken = answerWordCount <= 2;
|
||||
const likelyFragmentMismatch = nativeLooksSentence && answerLooksShortToken;
|
||||
if (nativeText && answerText && nativeText !== answerText && !likelyFragmentMismatch) {
|
||||
// Die answer ist normalerweise Bisaya, nativeWord ist Muttersprache
|
||||
// Nur hinzufügen, wenn sie unterschiedlich sind (verhindert "ko" -> "ko")
|
||||
vocabMap.set(`${nativeText}-${answerText}`, { learning: nativeText, reference: answerText });
|
||||
debugLog(`[importantVocab] Gap Fill extrahiert - Muttersprache:`, nativeText, `Bisaya:`, answerText);
|
||||
// Prefer full phrase reconstruction when we have matching "{gap}" placeholders.
|
||||
let reconstructed = '';
|
||||
if (parts.length === answers.length + 1) {
|
||||
const leftRaw = stripHints(parts[index] || '');
|
||||
const rightRaw = stripHints(parts[index + 1] || '');
|
||||
const leftCut = lastStopIndex(leftRaw);
|
||||
const leftTail = leftCut >= 0 ? leftRaw.slice(leftCut + 1) : leftRaw;
|
||||
const rightCut = firstStopIndex(rightRaw);
|
||||
const rightHead = rightCut >= 0 ? rightRaw.slice(0, rightCut) : rightRaw;
|
||||
reconstructed = `${leftTail}${answerText}${rightHead}`
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
const reconstructedIsUseful =
|
||||
reconstructed &&
|
||||
reconstructed !== answerText &&
|
||||
(reconstructed.includes(' ') || /[?!]/.test(reconstructed));
|
||||
|
||||
if (nativeText && (reconstructedIsUseful || answerText) && nativeText !== answerText && !likelyFragmentMismatch) {
|
||||
const targetText = reconstructedIsUseful ? reconstructed : answerText;
|
||||
// Sicherheitsfilter: wir wollen primär echte Phrasen, nicht einzelne Tokens als "Vokabelkarte".
|
||||
const targetWordCount = targetText.split(/\s+/).filter(Boolean).length;
|
||||
if (targetWordCount < 2 && !/[?!]/.test(targetText)) {
|
||||
debugLog(`[importantVocab] Gap Fill übersprungen - Ziel wirkt zu kurz:`, nativeText, targetText);
|
||||
return;
|
||||
}
|
||||
// Muttersprache (learning) -> Zielsprache (reference)
|
||||
vocabMap.set(`${nativeText}-${targetText}`, { learning: nativeText, reference: targetText });
|
||||
debugLog(`[importantVocab] Gap Fill extrahiert - Muttersprache:`, nativeText, `Bisaya:`, targetText);
|
||||
} else {
|
||||
debugLog(`[importantVocab] Gap Fill übersprungen - Satz/Fragment-Mismatch oder gleich:`, nativeText, answerText);
|
||||
debugLog(`[importantVocab] Gap Fill übersprungen - Satz/Fragment-Mismatch oder gleich:`, nativeText, answerText, reconstructed);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2478,6 +2589,7 @@ export default {
|
||||
this.exerciseRetryPending = false;
|
||||
this.exerciseRetryPendingSinceAttempts = 0;
|
||||
this.exerciseSequentialIndex = 0;
|
||||
this.dailyEnoughBannerDismissed = false;
|
||||
this.vocabOverviewPage = 1;
|
||||
this.exercisePreparationCompleted = false;
|
||||
this.lessonPrepStage = 0;
|
||||
@@ -3403,10 +3515,12 @@ export default {
|
||||
const didacticMode = String(this.lessonPedagogy?.didacticMode || '').toLowerCase();
|
||||
const isReviewLesson = ['review', 'vocab_review'].includes(lessonType)
|
||||
|| ['review', 'vocab_review', 'intensive_review'].includes(didacticMode);
|
||||
// Reviews sollen nicht nur aus Multiple Choice bestehen:
|
||||
// früherer Wechsel zu Typing, damit aktiver Abruf im Vordergrund steht.
|
||||
const switchAfterAttempts = isReviewLesson
|
||||
? Math.max(4, Math.ceil(this.trainerExerciseUnlockAttempts * 0.25))
|
||||
? Math.max(2, Math.ceil(this.trainerExerciseUnlockAttempts * 0.15))
|
||||
: this.trainerExerciseUnlockAttempts;
|
||||
const requiredSuccessRate = isReviewLesson ? 70 : 80;
|
||||
const requiredSuccessRate = isReviewLesson ? 60 : 80;
|
||||
|
||||
if (this.vocabTrainerMode === 'multiple_choice' && this.vocabTrainerTotalAttempts >= switchAfterAttempts) {
|
||||
const successRate = (this.vocabTrainerCorrect / this.vocabTrainerTotalAttempts) * 100;
|
||||
@@ -3617,20 +3731,17 @@ export default {
|
||||
this.vocabTrainerDirection = Math.random() < 0.5 ? 'L2R' : 'R2L';
|
||||
const allTrainerVocabs = [...this.trainableLessonVocab, ...this.vocabTrainerMixedPool];
|
||||
const prompt = this.vocabTrainerDirection === 'L2R' ? vocab.learning : vocab.reference;
|
||||
const acceptableAnswers = this.getEquivalentVocabAnswers(
|
||||
prompt,
|
||||
this.vocabTrainerDirection,
|
||||
allTrainerVocabs
|
||||
);
|
||||
// Akzeptiere mehrere Übersetzungen für denselben Prompt (z. B. "Bitte wiederholen" UND "Kannst du das wiederholen?").
|
||||
const acceptableAnswers = this.vocabTrainerDirection === 'L2R'
|
||||
? [vocab.reference]
|
||||
: (Array.isArray(vocab.learningVariants) && vocab.learningVariants.length > 0
|
||||
? vocab.learningVariants
|
||||
: [vocab.learning]);
|
||||
this.currentVocabQuestion = {
|
||||
vocab: vocab,
|
||||
prompt,
|
||||
answers: acceptableAnswers.length > 0
|
||||
? acceptableAnswers
|
||||
: [this.vocabTrainerDirection === 'L2R' ? vocab.reference : vocab.learning],
|
||||
answer: acceptableAnswers.length > 0
|
||||
? acceptableAnswers.join(' / ')
|
||||
: (this.vocabTrainerDirection === 'L2R' ? vocab.reference : vocab.learning),
|
||||
answers: acceptableAnswers.filter(Boolean),
|
||||
answer: acceptableAnswers.filter(Boolean).join(' / ') || (this.vocabTrainerDirection === 'L2R' ? vocab.reference : vocab.learning),
|
||||
key: this.getVocabKey(vocab),
|
||||
source: questionSource
|
||||
};
|
||||
@@ -4149,6 +4260,31 @@ export default {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.lesson-enough-banner {
|
||||
margin-bottom: 18px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(34, 96, 164, 0.18);
|
||||
background: rgba(235, 244, 255, 0.72);
|
||||
color: #21598f;
|
||||
}
|
||||
|
||||
.lesson-enough-banner strong,
|
||||
.lesson-enough-banner p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lesson-enough-banner p {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.lesson-enough-banner__actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lesson-overview-more {
|
||||
border-top: 1px solid rgba(160, 120, 40, 0.18);
|
||||
padding-top: 14px;
|
||||
|
||||
Reference in New Issue
Block a user