feat: Round 23 - Practice exercises, enhanced virtual character, inter-school federation, voice chat

- Practice exercises: AI-generated multiple choice questions, practice mode with score tracking
- Enhanced virtual character: More animations, accessories, AI chat integration, XP system
- Inter-school federation: School leaderboard, team scores, federation competitions
- Enhanced communication: Voice chat, file sharing, note sharing, teacher notes panel
- Added isFederation and federationSchedule fields to Competition model
- 170+ new i18n keys
- All lints pass
This commit is contained in:
Mustafa Cetin committed 2026-07-29 21:01:32 +00:00
1 parent 2f266a2143
commit 44ff170fca
12 files changed
+3655 -476

No files matched your search

+44 -1
View File
@@ -3110,11 +3110,13 @@ export interface CompetitionData {
rules: string | null;
isPublic: boolean;
isDemo: boolean;
isFederation: boolean;
federationSchedule: string | null;
createdById: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
school?: { id: string; name: string };
school?: { id: string; name: string; primaryColor?: string | null };
createdBy?: { id: string; firstName: string; lastName: string };
subject?: { id: string; name: string } | null;
_count?: { participants: number; rewards: number };
@@ -3202,6 +3204,47 @@ export function fetchCompetitions(
return apiGet<{ competitions: CompetitionData[]; total: number }>(`/api/competitions?${params.toString()}`);
}
export interface FederationLeaderboardEntry {
schoolId: string;
schoolName: string;
teamScore: number;
participantCount: number;
rank: number;
}
export function fetchFederationCompetitions(
category?: string,
schedule?: string,
status?: string,
limit?: number,
offset?: number
): Promise<{ competitions: CompetitionData[]; total: number; schoolLeaderboard: FederationLeaderboardEntry[] }> {
const params = new URLSearchParams();
if (category) params.set('category', category);
if (schedule) params.set('schedule', schedule);
if (status) params.set('status', status);
if (limit) params.set('limit', String(limit));
if (offset) params.set('offset', String(offset));
return apiGet<{ competitions: CompetitionData[]; total: number; schoolLeaderboard: FederationLeaderboardEntry[] }>(`/api/competitions/federation?${params.toString()}`);
}
export function createFederationCompetition(data: {
schoolId: string;
title: string;
description?: string | null;
category: string;
subjectId?: string | null;
schedule: string;
startDate: string;
endDate: string;
registrationDeadline?: string | null;
maxParticipants?: number | null;
rules?: string | null;
isPublic?: boolean;
}): Promise<CompetitionData> {
return apiPost<CompetitionData>('/api/competitions/federation', data);
}
export function fetchPublicCompetitions(
limit?: number,
offset?: number
+368
View File
@@ -4630,6 +4630,190 @@ const de: TranslationDict = {
'subjects.crawl_admin_only': 'Nur fuer Administratoren',
'subjects.crawl_progress': 'Crawl-Fortschritt',
'subjects.crawl_complete': 'Crawl abgeschlossen',
// ─── Practice Exercises ──────────────────────────────────────────────
'practice.start': 'Uebung starten',
'practice.generate_questions': 'Fragen mit KI generieren',
'practice.question': 'Frage',
'practice.correct': 'Richtig!',
'practice.incorrect': 'Leider falsch',
'practice.explanation': 'Erklaerung',
'practice.score': 'Punktzahl',
'practice.results': 'Ergebnisse',
'practice.retry': 'Nochmal versuchen',
'practice.retry_incorrect': 'Falsche Fragen wiederholen',
'practice.next_question': 'Naechste Frage',
'practice.finish': 'Uebung beenden',
'practice.generating': 'Fragen werden generiert...',
'practice.no_questions': 'Keine Fragen vorhanden',
'practice.no_questions_desc': 'Generiere Fragen mit KI oder waehle ein anderes Thema.',
'practice.celebration': 'Grossartig!',
'practice.try_again': 'Erneut versuchen',
'practice.question_count_label': 'Anzahl der Fragen',
'practice.difficulty_label': 'Schwierigkeit',
'practice.grade_level': 'Klassenstufe',
'practice.answered': 'Beantwortet',
'practice.correct_answers': 'Richtige Antworten',
'practice.incorrect_answers': 'Falsche Antworten',
'practice.percentage': 'Prozent',
'practice.detailed_breakdown': 'Detaillierte Auswertung',
'practice.your_answer': 'Deine Antwort',
'practice.correct_answer': 'Richtige Antwort',
'practice.rate_limit_reached': 'Tageslimit erreicht',
'practice.rate_limit_desc': 'Du hast das Limit von {limit} Fragen-Generierungen pro Tag erreicht. Bitte versuche es morgen erneut.',
'practice.generation_error': 'Fehler beim Generieren',
'practice.generation_error_desc': 'Die Fragen konnten nicht generiert werden. Bitte versuche es erneut.',
'practice.existing_questions': 'Vorhandene Fragen',
'practice.new_questions': 'Neue Fragen generieren',
'practice.option_a': 'A',
'practice.option_b': 'B',
'practice.option_c': 'C',
'practice.option_d': 'D',
'practice.perfect_score': 'Perfekt! Alle Fragen richtig!',
'practice.great_score': 'Sehr gut! Weiter so!',
'practice.good_score': 'Gut gemacht! Ein wenig mehr Uebung.',
'practice.needs_practice': 'Weiter ueben! Du schaffst das!',
'practice.xp_earned': '{xp} XP verdient',
// ─── Enhanced Virtual Character ──────────────────────────────────────
'character.accessories': 'Zubehoer',
'character.hat': 'Hut',
'character.glasses': 'Brille',
'character.scarf': 'Schal',
'character.cape': 'Umhang',
'character.wand': 'Zauberstab',
'character.unlock_level': 'Freischalten ab Level {level}',
'character.customize': 'Anpassen',
'character.color': 'Farbe',
'character.greeting': 'Hallo {name}!',
'character.daily_tip': 'Tipp des Tages',
'character.exam_reminder': 'Pruefungserinnerung',
'character.reaction_correct': 'Richtig! Weiter so!',
'character.reaction_incorrect': 'Das war leider falsch. Versuch es nochmal!',
'character.reaction_celebrating': 'Fantastisch! Du bist toll!',
'character.reaction_thinking': 'Hmm, lass mich darueber nachdenken...',
'character.chat': 'Mit {name} chatten',
'character.chat_placeholder': 'Frage mich etwas...',
'character.send': 'Senden',
'character.close_chat': 'Chat schliessen',
'character.mood_idle': 'Entspannt',
'character.mood_talking': 'Gespraechig',
'character.accessory_locked': 'Gesperrt',
'character.accessory_unlocked': 'Freigeschaltet',
'character.xp_to_next': 'Noch {xp} XP bis Level {level}',
'character.daily_tip_1': 'Regelmaessiges Lernen ist besser als langes Lernen am Stueck!',
'character.daily_tip_2': 'Mach alle 25 Minuten eine kurze Pause!',
'character.daily_tip_3': 'Wiederholung ist der Schluessel zum Erfolg!',
'character.daily_tip_4': 'Schreibe dir das Wichtigste auf!',
'character.daily_tip_5': 'Erklaere anderen, was du gelernt hast!',
'character.exam_reminder_msg': 'Vergiss nicht: Du hast bald eine Pruefung!',
'character.encouragement_1': 'Du machst das super!',
'character.encouragement_2': 'Weiter so, du bist auf dem richtigen Weg!',
'character.encouragement_3': 'Jeder Schritt zaehlt!',
'character.encouragement_4': 'Lass dich nicht entmutigen!',
'character.encouragement_5': 'Uebung macht den Meister!',
'character.confetti': 'Konfetti',
'character.level_up': 'Level aufgestiegen!',
'character.new_level': 'Du bist jetzt Level {level}!',
'character.animation_idle': 'Schwebend',
'character.animation_thinking': 'Nachdenkend',
'character.animation_celebrating': 'Feiernd',
'character.animation_sleeping': 'Schlafend',
'character.animation_talking': 'Sprechend',
'character.select_character': 'Charakter auswaehlen',
'character.preview': 'Vorschau',
'character.save': 'Speichern',
'character.saved': 'Gespeichert!',
'character.name': 'Name',
'character.level': 'Level',
'character.xp': 'XP',
'character.select': 'Auswaehlen',
'character.owl': 'Eule',
'character.dragon': 'Drache',
'character.robot': 'Roboter',
'character.cat': 'Katze',
'character.wizard': 'Magier',
'character.mood_happy': 'Froehlich',
'character.mood_excited': 'Aufgeregt',
'character.mood_thinking': 'Nachdenkend',
'character.mood_sleepy': 'Muede',
'character.mood_celebrating': 'Feiernd',
'character.greeting_morning': 'Guten Morgen!',
'character.greeting_afternoon': 'Guten Tag!',
'character.greeting_evening': 'Guten Abend!',
'character.idle_message_1': 'Lust auf eine Uebung?',
'character.idle_message_2': 'Lass uns etwas lernen!',
'character.idle_message_3': 'Bereit fuer eine Herausforderung?',
// ─── Federation (Inter-School Competition) ────────────────────────
'federation.title': 'Schul-Wettbewerbsverbund',
'federation.leaderboard': 'Bestenliste',
'federation.school_ranking': 'Schulrangliste',
'federation.team_score': 'Team-Punktzahl',
'federation.join': 'Teilnehmen',
'federation.create': 'Verbands-Wettbewerb erstellen',
'federation.math_olympiad': 'Mathematik-Olympiade',
'federation.science_bowl': 'Wissenschafts-Bowl',
'federation.language_quiz': 'Sprach-Quiz',
'federation.general_knowledge': 'Allgemeinwissen',
'federation.weekly': 'Woechentlich',
'federation.monthly': 'Monatlich',
'federation.quarterly': 'Vierteljaehrlich',
'federation.school_rank': 'Schulrang',
'federation.participants': 'Teilnehmer',
'federation.competition_type': 'Wettbewerbstyp',
'federation.schedule': 'Zeitplan',
'federation.no_district': 'Ihre Schule gehoert keinem Bezirk an',
'federation.no_competitions': 'Keine Verbands-Wettbewerbe vorhanden',
'federation.your_school': 'Ihre Schule',
'federation.points': 'Punkte',
'federation.top_students': 'Top-Schueler',
'federation.register_success': 'Erfolgreich angemeldet',
'federation.create_success': 'Verbands-Wettbewerb erstellt',
'federation.error_create': 'Fehler beim Erstellen des Wettbewerbs',
'federation.error_join': 'Fehler bei der Anmeldung',
'federation.description': 'Schulen im selben Bezirk treten gegeneinander an',
'federation.category_label': 'Kategorie',
'federation.district_schools': 'Bezirksschulen',
'federation.active_federation': 'Aktive Verbands-Wettbewerbe',
'federation.upcoming': 'Anstehende Wettbewerbe',
'federation.completed': 'Abgeschlossene Wettbewerbe',
// ─── Enhanced Communication ───────────────────────────────────────
'communication.voice_call': 'Sprachanruf',
'communication.voice_message': 'Sprachnachricht',
'communication.play': 'Abspielen',
'communication.share_notes': 'Notizen teilen',
'communication.select_notebook': 'Heft auswaehlen',
'communication.shared_notes': 'Geteilte Notizen',
'communication.upload_file': 'Datei hochladen',
'communication.drag_drop': 'Datei hierher ziehen oder klicken',
'communication.teacher_notes': 'Lehrer-Notizen',
'communication.private_notes': 'Private Notizen',
'communication.save_note': 'Notiz speichern',
'communication.notes_saved': 'Notiz gespeichert',
'communication.note_shared': 'Notiz geteilt',
'communication.file_shared': 'Datei geteilt',
'communication.recording': 'Aufnahme...',
'communication.record_voice': 'Sprachnachricht aufnehmen',
'communication.stop_recording': 'Aufnahme stoppen',
'communication.voice_duration': 'Dauer',
'communication.attach_file': 'Datei anhaengen',
'communication.supported_types': 'Unterstuetzte Formate: PDF, Bilder, Dokumente',
'communication.file_size_limit': 'Maximale Dateigroesse: 10 MB',
'communication.no_notebooks': 'Keine Hefte vorhanden',
'communication.notebook_pages': 'Seiten',
'communication.teacher_notes_panel': 'Lehrer-Notizen (nicht sichtbar fuer Schueler)',
'communication.teacher_notes_placeholder': 'Private Notizen zu diesem Gespraech...',
'communication.notes_history': 'Notizverlauf',
'communication.add_note': 'Notiz hinzufuegen',
'communication.delete_note': 'Notiz loeschen',
'communication.note_added': 'Notiz hinzugefuegt',
'communication.note_deleted': 'Notiz geloescht',
'communication.drop_zone': 'Datei ablegen',
'communication.voice_recording_error': 'Mikrofonzugriff verweigert',
'communication.play_voice': 'Sprachnachricht abspielen',
'communication.pause_voice': 'Sprachnachricht pausieren',
};
const en: TranslationDict = {
@@ -9258,6 +9442,190 @@ const en: TranslationDict = {
'subjects.crawl_admin_only': 'Admin Only',
'subjects.crawl_progress': 'Crawl Progress',
'subjects.crawl_complete': 'Crawl Complete',
// ─── Practice Exercises ──────────────────────────────────────────────
'practice.start': 'Start Practice',
'practice.generate_questions': 'Generate Questions with AI',
'practice.question': 'Question',
'practice.correct': 'Correct!',
'practice.incorrect': 'Incorrect',
'practice.explanation': 'Explanation',
'practice.score': 'Score',
'practice.results': 'Results',
'practice.retry': 'Try Again',
'practice.retry_incorrect': 'Retry Incorrect Questions',
'practice.next_question': 'Next Question',
'practice.finish': 'Finish Practice',
'practice.generating': 'Generating questions...',
'practice.no_questions': 'No Questions Available',
'practice.no_questions_desc': 'Generate questions with AI or choose a different topic.',
'practice.celebration': 'Awesome!',
'practice.try_again': 'Try Again',
'practice.question_count_label': 'Number of Questions',
'practice.difficulty_label': 'Difficulty',
'practice.grade_level': 'Grade Level',
'practice.answered': 'Answered',
'practice.correct_answers': 'Correct Answers',
'practice.incorrect_answers': 'Incorrect Answers',
'practice.percentage': 'Percentage',
'practice.detailed_breakdown': 'Detailed Breakdown',
'practice.your_answer': 'Your Answer',
'practice.correct_answer': 'Correct Answer',
'practice.rate_limit_reached': 'Daily Limit Reached',
'practice.rate_limit_desc': 'You have reached the limit of {limit} question generations per day. Please try again tomorrow.',
'practice.generation_error': 'Generation Error',
'practice.generation_error_desc': 'Questions could not be generated. Please try again.',
'practice.existing_questions': 'Existing Questions',
'practice.new_questions': 'Generate New Questions',
'practice.option_a': 'A',
'practice.option_b': 'B',
'practice.option_c': 'C',
'practice.option_d': 'D',
'practice.perfect_score': 'Perfect! All questions correct!',
'practice.great_score': 'Great job! Keep it up!',
'practice.good_score': 'Good work! A little more practice.',
'practice.needs_practice': 'Keep practicing! You can do it!',
'practice.xp_earned': '{xp} XP earned',
// ─── Enhanced Virtual Character ──────────────────────────────────────
'character.accessories': 'Accessories',
'character.hat': 'Hat',
'character.glasses': 'Glasses',
'character.scarf': 'Scarf',
'character.cape': 'Cape',
'character.wand': 'Wand',
'character.unlock_level': 'Unlocks at Level {level}',
'character.customize': 'Customize',
'character.color': 'Color',
'character.greeting': 'Hello {name}!',
'character.daily_tip': 'Tip of the Day',
'character.exam_reminder': 'Exam Reminder',
'character.reaction_correct': 'Correct! Keep going!',
'character.reaction_incorrect': 'That was incorrect. Try again!',
'character.reaction_celebrating': 'Fantastic! You are amazing!',
'character.reaction_thinking': 'Hmm, let me think about that...',
'character.chat': 'Chat with {name}',
'character.chat_placeholder': 'Ask me something...',
'character.send': 'Send',
'character.close_chat': 'Close Chat',
'character.mood_idle': 'Relaxed',
'character.mood_talking': 'Talkative',
'character.accessory_locked': 'Locked',
'character.accessory_unlocked': 'Unlocked',
'character.xp_to_next': '{xp} XP until Level {level}',
'character.daily_tip_1': 'Regular studying is better than cramming!',
'character.daily_tip_2': 'Take a short break every 25 minutes!',
'character.daily_tip_3': 'Repetition is the key to success!',
'character.daily_tip_4': 'Write down the most important things!',
'character.daily_tip_5': 'Explain to others what you have learned!',
'character.exam_reminder_msg': 'Don\'t forget: You have an exam soon!',
'character.encouragement_1': 'You are doing great!',
'character.encouragement_2': 'Keep going, you are on the right track!',
'character.encouragement_3': 'Every step counts!',
'character.encouragement_4': 'Don\'t be discouraged!',
'character.encouragement_5': 'Practice makes perfect!',
'character.confetti': 'Confetti',
'character.level_up': 'Level Up!',
'character.new_level': 'You are now Level {level}!',
'character.animation_idle': 'Floating',
'character.animation_thinking': 'Thinking',
'character.animation_celebrating': 'Celebrating',
'character.animation_sleeping': 'Sleeping',
'character.animation_talking': 'Talking',
'character.select_character': 'Select Character',
'character.preview': 'Preview',
'character.save': 'Save',
'character.saved': 'Saved!',
'character.name': 'Name',
'character.level': 'Level',
'character.xp': 'XP',
'character.select': 'Select',
'character.owl': 'Owl',
'character.dragon': 'Dragon',
'character.robot': 'Robot',
'character.cat': 'Cat',
'character.wizard': 'Wizard',
'character.mood_happy': 'Happy',
'character.mood_excited': 'Excited',
'character.mood_thinking': 'Thinking',
'character.mood_sleepy': 'Sleepy',
'character.mood_celebrating': 'Celebrating',
'character.greeting_morning': 'Good Morning!',
'character.greeting_afternoon': 'Good Afternoon!',
'character.greeting_evening': 'Good Evening!',
'character.idle_message_1': 'Want to practice?',
'character.idle_message_2': 'Let\'s learn something!',
'character.idle_message_3': 'Ready for a challenge?',
// ─── Federation (Inter-School Competition) ────────────────────────
'federation.title': 'Inter-School Competition Federation',
'federation.leaderboard': 'Leaderboard',
'federation.school_ranking': 'School Ranking',
'federation.team_score': 'Team Score',
'federation.join': 'Join Competition',
'federation.create': 'Create Federation Competition',
'federation.math_olympiad': 'Math Olympiad',
'federation.science_bowl': 'Science Bowl',
'federation.language_quiz': 'Language Quiz',
'federation.general_knowledge': 'General Knowledge',
'federation.weekly': 'Weekly',
'federation.monthly': 'Monthly',
'federation.quarterly': 'Quarterly',
'federation.school_rank': 'School Rank',
'federation.participants': 'Participants',
'federation.competition_type': 'Competition Type',
'federation.schedule': 'Schedule',
'federation.no_district': 'Your school is not part of a district',
'federation.no_competitions': 'No federation competitions available',
'federation.your_school': 'Your School',
'federation.points': 'Points',
'federation.top_students': 'Top Students',
'federation.register_success': 'Successfully registered',
'federation.create_success': 'Federation competition created',
'federation.error_create': 'Error creating competition',
'federation.error_join': 'Error joining competition',
'federation.description': 'Schools in the same district compete against each other',
'federation.category_label': 'Category',
'federation.district_schools': 'District Schools',
'federation.active_federation': 'Active Federation Competitions',
'federation.upcoming': 'Upcoming Competitions',
'federation.completed': 'Completed Competitions',
// ─── Enhanced Communication ───────────────────────────────────────
'communication.voice_call': 'Voice Call',
'communication.voice_message': 'Voice Message',
'communication.play': 'Play',
'communication.share_notes': 'Share Notes',
'communication.select_notebook': 'Select Notebook',
'communication.shared_notes': 'Shared Notes',
'communication.upload_file': 'Upload File',
'communication.drag_drop': 'Drag and drop file here or click',
'communication.teacher_notes': 'Teacher Notes',
'communication.private_notes': 'Private Notes',
'communication.save_note': 'Save Note',
'communication.notes_saved': 'Note saved',
'communication.note_shared': 'Note shared',
'communication.file_shared': 'File shared',
'communication.recording': 'Recording...',
'communication.record_voice': 'Record voice message',
'communication.stop_recording': 'Stop recording',
'communication.voice_duration': 'Duration',
'communication.attach_file': 'Attach file',
'communication.supported_types': 'Supported formats: PDF, images, documents',
'communication.file_size_limit': 'Maximum file size: 10 MB',
'communication.no_notebooks': 'No notebooks available',
'communication.notebook_pages': 'Pages',
'communication.teacher_notes_panel': 'Teacher Notes (not visible to student)',
'communication.teacher_notes_placeholder': 'Private notes about this conversation...',
'communication.notes_history': 'Notes History',
'communication.add_note': 'Add Note',
'communication.delete_note': 'Delete Note',
'communication.note_added': 'Note added',
'communication.note_deleted': 'Note deleted',
'communication.drop_zone': 'Drop file here',
'communication.voice_recording_error': 'Microphone access denied',
'communication.play_voice': 'Play voice message',
'communication.pause_voice': 'Pause voice message',
};
const translations: Record<string, TranslationDict> = { de, en };