feat: add A2-B1 course expansion features including listening activities and conversation sessions
- Updated VocabCourseView.vue to display CEFR level for courses. - Enhanced VocabLessonView.vue with new sections for listening activities and conversation labs, including audio controls and transcript toggling. - Created migration to add new columns to vocab_course and new tables for conversation sessions and listening activities. - Implemented models for vocab_conversation_session and vocab_listening_activity. - Developed scripts to create new A2 and B1 bridge courses with structured lesson plans. - Documented the A2-B1 expansion plan detailing course structure, learning goals, and assessment criteria.
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
|
||||
<div class="course-info surface-card">
|
||||
<span>{{ $t('socialnetwork.vocab.courses.difficulty') }}: {{ course.difficultyLevel }}</span>
|
||||
<span v-if="course.cefrLevel">GER: {{ course.cefrLevel }}</span>
|
||||
<span v-if="course.isPublic">{{ $t('socialnetwork.vocab.courses.public') }}</span>
|
||||
<span v-if="course.shareCode && isOwner" class="share-code">
|
||||
{{ $t('socialnetwork.vocab.courses.shareCode') }}: <code>{{ course.shareCode }}</code>
|
||||
|
||||
@@ -545,6 +545,40 @@
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<section v-if="listeningActivities.length" class="didactic-card listening-lab">
|
||||
<h4>Hörtraining</h4>
|
||||
<article v-for="activity in listeningActivities" :key="activity.id" class="listening-lab__activity">
|
||||
<p><strong>{{ activity.speed === 'natural' ? 'Natürliches Tempo' : activity.speed }}</strong></p>
|
||||
<audio :src="activity.audioUrl" controls preload="metadata" />
|
||||
<button type="button" class="button-secondary" @click="toggleTranscript(activity)">
|
||||
{{ listeningTranscripts[activity.id] ? 'Transkript ausblenden' : 'Transkript anzeigen' }}
|
||||
</button>
|
||||
<p v-if="listeningTranscripts[activity.id]" class="listening-lab__transcript">{{ listeningTranscripts[activity.id].transcript }}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section v-if="assistantAvailable" class="didactic-card conversation-lab">
|
||||
<h4>Gesprächslabor</h4>
|
||||
<p>Übe ein längeres Rollengespräch. Korrekturen kommen erst nach der Sitzung.</p>
|
||||
<template v-if="!conversationSession">
|
||||
<label><span>Rolle der KI</span><input v-model="conversationScenario.role" placeholder="z. B. Nachbarin" /></label>
|
||||
<label><span>Gesprächsziel</span><input v-model="conversationScenario.goal" placeholder="z. B. einen Termin verschieben" /></label>
|
||||
<button type="button" @click="startConversation" :disabled="conversationSubmitting">Gespräch starten</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="language-assistant-chat">
|
||||
<article v-for="(turn, index) in conversationSession.turns" :key="`conversation-${index}`" class="assistant-message" :class="`assistant-message--${turn.role}`">
|
||||
<strong>{{ turn.role === 'assistant' ? 'Gesprächspartner' : 'Du' }}</strong><p>{{ turn.content }}</p>
|
||||
</article>
|
||||
</div>
|
||||
<textarea v-model="conversationInput" rows="3" placeholder="Dein Gesprächsbeitrag" />
|
||||
<button type="button" @click="sendConversationTurn" :disabled="conversationSubmitting || !conversationInput.trim()">Antworten</button>
|
||||
<button type="button" class="button-secondary" @click="completeConversation" :disabled="conversationSubmitting">Gespräch beenden</button>
|
||||
<p v-if="conversationSession.summary">{{ conversationSession.summary.nextStep }}</p>
|
||||
</template>
|
||||
<p v-if="conversationError" class="form-error">{{ conversationError }}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Übungen-Tab (Kapitel-Prüfung) -->
|
||||
@@ -1163,6 +1197,13 @@ export default {
|
||||
assistantWaitElapsedSeconds: 0,
|
||||
assistantWaitTimer: null,
|
||||
isAssistantFocused: false,
|
||||
listeningActivities: [],
|
||||
listeningTranscripts: {},
|
||||
conversationSession: null,
|
||||
conversationInput: '',
|
||||
conversationScenario: { role: '', goal: '', title: '', level: 'A2', difficulty: 'guided' },
|
||||
conversationSubmitting: false,
|
||||
conversationError: '',
|
||||
nextLessonId: null,
|
||||
showCompletionDialog: false,
|
||||
showErrorDialog: false,
|
||||
@@ -2798,6 +2839,11 @@ export default {
|
||||
this.assistantMessages = [];
|
||||
this.assistantInput = '';
|
||||
this.assistantError = '';
|
||||
this.listeningActivities = [];
|
||||
this.listeningTranscripts = {};
|
||||
this.conversationSession = null;
|
||||
this.conversationInput = '';
|
||||
this.conversationError = '';
|
||||
this.exerciseRetryPending = false;
|
||||
this.exerciseRetryPendingSinceAttempts = 0;
|
||||
this.exerciseSequentialIndex = 0;
|
||||
@@ -2842,6 +2888,7 @@ export default {
|
||||
try {
|
||||
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}`);
|
||||
this.lesson = res.data;
|
||||
await this.loadListeningActivities();
|
||||
await this.loadCourseLanguageNames();
|
||||
await this.loadCourseProgressForBoost();
|
||||
debugLog('[VocabLessonView] Geladene Lektion:', this.lesson?.id, this.lesson?.title);
|
||||
@@ -2963,6 +3010,71 @@ export default {
|
||||
openLanguageAssistantSettings() {
|
||||
this.$router.push('/settings/language-assistant');
|
||||
},
|
||||
async loadListeningActivities() {
|
||||
try {
|
||||
const { data } = await apiClient.get(`/api/vocab/lessons/${this.lessonId}/listening-activities`);
|
||||
this.listeningActivities = Array.isArray(data) ? data : [];
|
||||
} catch (e) {
|
||||
this.listeningActivities = [];
|
||||
}
|
||||
},
|
||||
async toggleTranscript(activity) {
|
||||
if (this.listeningTranscripts[activity.id]) {
|
||||
const next = { ...this.listeningTranscripts };
|
||||
delete next[activity.id];
|
||||
this.listeningTranscripts = next;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data } = await apiClient.get(`/api/vocab/lessons/${this.lessonId}/listening-activities`, { params: { includeTranscript: true } });
|
||||
const full = (data || []).find((item) => item.id === activity.id);
|
||||
if (full?.transcript) this.listeningTranscripts = { ...this.listeningTranscripts, [activity.id]: full };
|
||||
} catch (e) {
|
||||
// Die Lektion bleibt auch ohne Transkript nutzbar.
|
||||
}
|
||||
},
|
||||
async startConversation() {
|
||||
if (!this.conversationScenario.role.trim() || !this.conversationScenario.goal.trim()) return;
|
||||
this.conversationSubmitting = true;
|
||||
this.conversationError = '';
|
||||
try {
|
||||
const { data } = await apiClient.post(`/api/vocab/lessons/${this.lessonId}/conversation-sessions`, {
|
||||
scenario: { ...this.conversationScenario, title: this.lesson?.title || 'Alltagssituation' }
|
||||
});
|
||||
this.conversationSession = data;
|
||||
} catch (e) {
|
||||
this.conversationError = e.response?.data?.error || e.message;
|
||||
} finally {
|
||||
this.conversationSubmitting = false;
|
||||
}
|
||||
},
|
||||
async sendConversationTurn() {
|
||||
const message = this.conversationInput.trim();
|
||||
if (!message || !this.conversationSession) return;
|
||||
this.conversationSubmitting = true;
|
||||
this.conversationError = '';
|
||||
try {
|
||||
const { data } = await apiClient.post(`/api/vocab/conversation-sessions/${this.conversationSession.id}/turns`, { message });
|
||||
this.conversationSession = data.session;
|
||||
this.conversationInput = '';
|
||||
} catch (e) {
|
||||
this.conversationError = e.response?.data?.error || e.message;
|
||||
} finally {
|
||||
this.conversationSubmitting = false;
|
||||
}
|
||||
},
|
||||
async completeConversation() {
|
||||
if (!this.conversationSession) return;
|
||||
this.conversationSubmitting = true;
|
||||
try {
|
||||
const { data } = await apiClient.post(`/api/vocab/conversation-sessions/${this.conversationSession.id}/complete`);
|
||||
this.conversationSession = data;
|
||||
} catch (e) {
|
||||
this.conversationError = e.response?.data?.error || e.message;
|
||||
} finally {
|
||||
this.conversationSubmitting = false;
|
||||
}
|
||||
},
|
||||
buildAssistantPrompt(preset) {
|
||||
const lessonTitle = this.lesson?.title || this.$t('socialnetwork.vocab.courses.thisLesson');
|
||||
const firstPattern = this.lessonDidactics.corePatterns?.[0];
|
||||
|
||||
Reference in New Issue
Block a user