Add language assistant settings and related features: Introduce new routes and controller methods for managing language assistant settings, including retrieval and saving of LLM configurations. Update navigation structure to include language assistant options. Enhance vocab course model to support additional learning attributes such as learning goals and core patterns. Update SQL scripts to reflect new database schema changes for vocab courses. Improve localization for language assistant settings in German and English.
This commit is contained in:
@@ -30,26 +30,80 @@
|
||||
|
||||
<!-- Lernen-Tab -->
|
||||
<div v-if="activeTab === 'learn'" class="learn-section">
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.learnVocabulary') }}</h3>
|
||||
|
||||
<!-- Lektions-Beschreibung -->
|
||||
<div v-if="lesson && lesson.description" class="lesson-description-box">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.lessonDescription') }}</h4>
|
||||
<p>{{ lesson.description }}</p>
|
||||
<div class="lesson-overview-card">
|
||||
<div>
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.learnVocabulary') }}</h3>
|
||||
<p class="lesson-overview-text">
|
||||
{{ $t('socialnetwork.vocab.courses.lessonOverviewText') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="lesson-meta-grid">
|
||||
<div class="lesson-meta-item">
|
||||
<span class="lesson-meta-label">{{ $t('socialnetwork.vocab.courses.lessonTypeLabel') }}</span>
|
||||
<strong>{{ getLessonTypeLabel(lesson.lessonType) }}</strong>
|
||||
</div>
|
||||
<div class="lesson-meta-item">
|
||||
<span class="lesson-meta-label">{{ $t('socialnetwork.vocab.courses.recommendedDuration') }}</span>
|
||||
<strong>{{ formatTargetMinutes(lesson.targetMinutes) }}</strong>
|
||||
</div>
|
||||
<div class="lesson-meta-item">
|
||||
<span class="lesson-meta-label">{{ $t('socialnetwork.vocab.courses.exerciseLoad') }}</span>
|
||||
<strong>{{ effectiveExercises?.length || 0 }} {{ $t('socialnetwork.vocab.courses.exercisesShort') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kulturelle Notizen -->
|
||||
<div v-if="lesson && lesson.culturalNotes" class="cultural-notes">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.culturalNotes') }}</h4>
|
||||
<p>{{ lesson.culturalNotes }}</p>
|
||||
</div>
|
||||
<div class="learn-grid">
|
||||
<div v-if="lesson && lesson.description" class="lesson-description-box">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.lessonDescription') }}</h4>
|
||||
<p>{{ lesson.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Grammatik-Erklärungen -->
|
||||
<div v-if="grammarExplanations && grammarExplanations.length > 0" class="grammar-explanations">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.grammarExplanations') }}</h4>
|
||||
<div v-for="(explanation, index) in grammarExplanations" :key="index" class="grammar-explanation-item">
|
||||
<strong>{{ explanation.title }}</strong>
|
||||
<p>{{ explanation.text }}</p>
|
||||
<div v-if="lessonDidactics.learningGoals.length > 0" class="didactic-card">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.learningGoals') }}</h4>
|
||||
<ul class="didactic-list">
|
||||
<li v-for="(goal, index) in lessonDidactics.learningGoals" :key="'goal-' + index">{{ goal }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="lessonDidactics.corePatterns.length > 0" class="didactic-card">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.corePatterns') }}</h4>
|
||||
<div class="pattern-list">
|
||||
<div v-for="(pattern, index) in lessonDidactics.corePatterns" :key="'pattern-' + index" class="pattern-item">
|
||||
{{ pattern }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="lessonDidactics.grammarFocus.length > 0" class="grammar-explanations didactic-card">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.grammarExplanations') }}</h4>
|
||||
<div v-for="(explanation, index) in lessonDidactics.grammarFocus" :key="'grammar-' + index" class="grammar-explanation-item">
|
||||
<strong>{{ explanation.title || $t('socialnetwork.vocab.courses.grammarImpulse') }}</strong>
|
||||
<p>{{ explanation.text }}</p>
|
||||
<p v-if="explanation.example" class="grammar-example">{{ explanation.example }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="lessonDidactics.speakingPrompts.length > 0" class="didactic-card">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.speakingTasks') }}</h4>
|
||||
<div v-for="(prompt, index) in lessonDidactics.speakingPrompts" :key="'speaking-' + index" class="speaking-prompt-item">
|
||||
<strong>{{ prompt.title || $t('socialnetwork.vocab.courses.speakingPrompt') }}</strong>
|
||||
<p>{{ prompt.prompt }}</p>
|
||||
<p v-if="prompt.cue" class="speaking-cue">{{ prompt.cue }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="lessonDidactics.practicalTasks.length > 0" class="didactic-card">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.practicalTasks') }}</h4>
|
||||
<div v-for="(task, index) in lessonDidactics.practicalTasks" :key="'task-' + index" class="practical-task-item">
|
||||
<strong>{{ task.title }}</strong>
|
||||
<p>{{ task.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="lesson && lesson.culturalNotes" class="cultural-notes didactic-card">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.culturalNotes') }}</h4>
|
||||
<p>{{ lesson.culturalNotes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -253,6 +307,94 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="getExerciseType(exercise) === 'sentence_building'" class="sentence-building-exercise">
|
||||
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
|
||||
<div v-if="getQuestionData(exercise)?.tokens?.length" class="token-list">
|
||||
<span v-for="(token, index) in getQuestionData(exercise).tokens" :key="index" class="token-chip">{{ token }}</span>
|
||||
</div>
|
||||
<input
|
||||
v-model="exerciseAnswers[exercise.id]"
|
||||
:placeholder="$t('socialnetwork.vocab.courses.buildSentencePlaceholder')"
|
||||
class="transformation-input"
|
||||
/>
|
||||
<button @click="checkAnswer(exercise.id)" :disabled="!exerciseAnswers[exercise.id]">
|
||||
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
||||
</button>
|
||||
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
|
||||
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
|
||||
<p v-if="exerciseResults[exercise.id].correctAnswer" class="correct-answer">
|
||||
{{ $t('socialnetwork.vocab.courses.modelSentence') }}: {{ exerciseResults[exercise.id].correctAnswer }}
|
||||
</p>
|
||||
<p v-if="exerciseResults[exercise.id].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="getExerciseType(exercise) === 'dialog_completion'" class="dialog-completion-exercise">
|
||||
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
|
||||
<div v-if="getQuestionData(exercise)?.dialog" class="dialog-snippet">
|
||||
<p v-for="(line, index) in getQuestionData(exercise).dialog" :key="index">{{ line }}</p>
|
||||
</div>
|
||||
<input
|
||||
v-model="exerciseAnswers[exercise.id]"
|
||||
:placeholder="$t('socialnetwork.vocab.courses.completeDialogPlaceholder')"
|
||||
class="transformation-input"
|
||||
/>
|
||||
<button @click="checkAnswer(exercise.id)" :disabled="!exerciseAnswers[exercise.id]">
|
||||
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
||||
</button>
|
||||
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
|
||||
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
|
||||
<p v-if="exerciseResults[exercise.id].correctAnswer" class="correct-answer">
|
||||
{{ $t('socialnetwork.vocab.courses.modelDialogLine') }}: {{ exerciseResults[exercise.id].correctAnswer }}
|
||||
</p>
|
||||
<p v-if="exerciseResults[exercise.id].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="getExerciseType(exercise) === 'situational_response'" class="situational-response-exercise">
|
||||
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
|
||||
<textarea
|
||||
v-model="exerciseAnswers[exercise.id]"
|
||||
:placeholder="$t('socialnetwork.vocab.courses.situationalResponsePlaceholder')"
|
||||
class="response-textarea"
|
||||
/>
|
||||
<button @click="checkAnswer(exercise.id)" :disabled="!exerciseAnswers[exercise.id]">
|
||||
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
||||
</button>
|
||||
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
|
||||
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
|
||||
<p v-if="exerciseResults[exercise.id].correctAnswer" class="correct-answer">
|
||||
{{ $t('socialnetwork.vocab.courses.modelResponse') }}: {{ exerciseResults[exercise.id].correctAnswer }}
|
||||
</p>
|
||||
<p v-if="exerciseResults[exercise.id].alternatives && exerciseResults[exercise.id].alternatives.length > 0" class="alternatives">
|
||||
{{ $t('socialnetwork.vocab.courses.keywords') }}: {{ exerciseResults[exercise.id].alternatives.join(', ') }}
|
||||
</p>
|
||||
<p v-if="exerciseResults[exercise.id].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="getExerciseType(exercise) === 'pattern_drill'" class="pattern-drill-exercise">
|
||||
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
|
||||
<p v-if="getQuestionData(exercise)?.pattern" class="pattern-drill-hint">
|
||||
{{ $t('socialnetwork.vocab.courses.patternPrompt') }}: {{ getQuestionData(exercise).pattern }}
|
||||
</p>
|
||||
<input
|
||||
v-model="exerciseAnswers[exercise.id]"
|
||||
:placeholder="$t('socialnetwork.vocab.courses.patternDrillPlaceholder')"
|
||||
class="transformation-input"
|
||||
/>
|
||||
<button @click="checkAnswer(exercise.id)" :disabled="!exerciseAnswers[exercise.id]">
|
||||
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
||||
</button>
|
||||
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
|
||||
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
|
||||
<p v-if="exerciseResults[exercise.id].correctAnswer" class="correct-answer">
|
||||
{{ $t('socialnetwork.vocab.courses.modelPattern') }}: {{ exerciseResults[exercise.id].correctAnswer }}
|
||||
</p>
|
||||
<p v-if="exerciseResults[exercise.id].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reading Aloud Übung -->
|
||||
<div v-else-if="getExerciseType(exercise) === 'reading_aloud'" class="reading-aloud-exercise">
|
||||
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
|
||||
@@ -543,6 +685,15 @@ export default {
|
||||
console.error('Fehler in importantVocab computed property:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
lessonDidactics() {
|
||||
return this.lesson?.didactics || {
|
||||
learningGoals: [],
|
||||
corePatterns: [],
|
||||
grammarFocus: [],
|
||||
speakingPrompts: [],
|
||||
practicalTasks: []
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -787,10 +938,31 @@ export default {
|
||||
5: 'conjugation',
|
||||
6: 'declension',
|
||||
7: 'reading_aloud',
|
||||
8: 'speaking_from_memory'
|
||||
8: 'speaking_from_memory',
|
||||
9: 'dialog_completion',
|
||||
10: 'situational_response',
|
||||
11: 'pattern_drill'
|
||||
};
|
||||
return typeMap[exercise.exerciseTypeId] || 'unknown';
|
||||
},
|
||||
getLessonTypeLabel(lessonType) {
|
||||
const labels = {
|
||||
vocab: this.$t('socialnetwork.vocab.courses.lessonTypeVocab'),
|
||||
grammar: this.$t('socialnetwork.vocab.courses.lessonTypeGrammar'),
|
||||
conversation: this.$t('socialnetwork.vocab.courses.lessonTypeConversation'),
|
||||
culture: this.$t('socialnetwork.vocab.courses.lessonTypeCulture'),
|
||||
review: this.$t('socialnetwork.vocab.courses.lessonTypeReview'),
|
||||
vocab_review: this.$t('socialnetwork.vocab.courses.lessonTypeReview')
|
||||
};
|
||||
return labels[lessonType] || lessonType || this.$t('socialnetwork.vocab.courses.lessonTypeVocab');
|
||||
},
|
||||
formatTargetMinutes(targetMinutes) {
|
||||
const minutes = Number(targetMinutes);
|
||||
if (!minutes) {
|
||||
return this.$t('socialnetwork.vocab.courses.durationFlexible');
|
||||
}
|
||||
return this.$t('socialnetwork.vocab.courses.durationMinutes', { minutes });
|
||||
},
|
||||
getQuestionData(exercise) {
|
||||
if (!exercise.questionData) return null;
|
||||
return typeof exercise.questionData === 'string'
|
||||
@@ -861,9 +1033,11 @@ export default {
|
||||
} else if (exerciseType === 'multiple_choice') {
|
||||
// Multiple Choice: Index als Zahl
|
||||
answer = Number(answer);
|
||||
} else if (exerciseType === 'transformation') {
|
||||
} else if (exerciseType === 'transformation' || exerciseType === 'sentence_building' || exerciseType === 'dialog_completion' || exerciseType === 'pattern_drill') {
|
||||
// Transformation: String
|
||||
answer = String(answer || '').trim();
|
||||
} else if (exerciseType === 'situational_response') {
|
||||
answer = String(answer || '').trim();
|
||||
} else if (exerciseType === 'reading_aloud' || exerciseType === 'speaking_from_memory') {
|
||||
// Reading Aloud / Speaking From Memory: Verwende erkannten Text
|
||||
answer = this.recognizedText[exerciseId] || String(answer || '').trim();
|
||||
@@ -1453,6 +1627,126 @@ export default {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.lesson-overview-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(135deg, #fff8eb 0%, #f7efe2 100%);
|
||||
border: 1px solid rgba(160, 120, 40, 0.18);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.lesson-overview-text {
|
||||
margin: 8px 0 0;
|
||||
color: #5b4b2f;
|
||||
}
|
||||
|
||||
.lesson-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(130px, 1fr));
|
||||
gap: 12px;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.lesson-meta-item {
|
||||
padding: 12px 14px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.lesson-meta-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.82rem;
|
||||
color: #7a6848;
|
||||
}
|
||||
|
||||
.learn-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.didactic-card,
|
||||
.lesson-description-box,
|
||||
.cultural-notes {
|
||||
padding: 18px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e7e7e7;
|
||||
}
|
||||
|
||||
.didactic-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.didactic-list li + li {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pattern-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pattern-item {
|
||||
padding: 12px 14px;
|
||||
border-left: 4px solid #d2831f;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.grammar-example,
|
||||
.speaking-cue,
|
||||
.pattern-drill-hint {
|
||||
margin-top: 8px;
|
||||
color: #66553a;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.speaking-prompt-item + .speaking-prompt-item,
|
||||
.practical-task-item + .practical-task-item {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.token-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.token-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
background: #eef3ff;
|
||||
border: 1px solid #cfdbff;
|
||||
border-radius: 999px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.dialog-snippet {
|
||||
margin: 14px 0;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.response-textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 12px;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 8px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
@@ -2023,7 +2317,11 @@ export default {
|
||||
|
||||
/* Reading Aloud & Speaking From Memory Styles */
|
||||
.reading-aloud-exercise,
|
||||
.speaking-from-memory-exercise {
|
||||
.speaking-from-memory-exercise,
|
||||
.sentence-building-exercise,
|
||||
.dialog-completion-exercise,
|
||||
.situational-response-exercise,
|
||||
.pattern-drill-exercise {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
@@ -2229,4 +2527,15 @@ export default {
|
||||
.dialog-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.lesson-overview-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.lesson-meta-grid {
|
||||
grid-template-columns: 1fr;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user