feat: JLPT levels for Japanese, custom practice scenarios, UI improvements

- Add separate level systems: CEFR (A1-C2) for European languages, JLPT (N5-N1) for Japanese
- Store levels per language in new `levels_by_language` JSON field
- Add custom scenario option in AI practice mode
- Show action buttons after practice ends (new dialogue, tasks, words)
- Fix level display across all handlers to use correct level system
- Add Alembic migration for levels_by_language field
- Update all locale files (ru, en, ja) with new keys

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-05 14:30:24 +03:00
parent 8bf3504d8d
commit 99deaafcbf
17 changed files with 983 additions and 308 deletions

View File

@@ -490,42 +490,62 @@ User: {user_message}
"suggestions": ["Sure!", "Well...", "Actually..."]
}
async def generate_level_test(self) -> List[Dict]:
async def generate_level_test(self, learning_language: str = "en") -> List[Dict]:
"""
Сгенерировать тест для определения уровня английского
Сгенерировать тест для определения уровня языка
Args:
learning_language: Язык изучения (en, es, de, fr, ja)
Returns:
Список из 7 вопросов разной сложности
"""
prompt = """Создай тест из 7 вопросов для определения уровня английского языка (A1-C2).
# Определяем систему уровней и язык для промпта
if learning_language == "ja":
level_system = "JLPT (N5-N1)"
language_name = "японского"
levels_req = """- Вопросы 1-2: уровень N5 (базовый)
- Вопросы 3-4: уровень N4-N3 (элементарный-средний)
- Вопросы 5-6: уровень N2 (продвинутый)
- Вопрос 7: уровень N1 (профессиональный)"""
level_example = "N5"
else:
level_system = "CEFR (A1-C2)"
lang_names = {"en": "английского", "es": "испанского", "de": "немецкого", "fr": "французского"}
language_name = lang_names.get(learning_language, "английского")
levels_req = """- Вопросы 1-2: уровень A1 (базовый)
- Вопросы 3-4: уровень A2-B1 (элементарный-средний)
- Вопросы 5-6: уровень B2-C1 (продвинутый)
- Вопрос 7: уровень C2 (профессиональный)"""
level_example = "A1"
prompt = f"""Создай тест из 7 вопросов для определения уровня {language_name} языка ({level_system}).
Верни ответ в формате JSON:
{
{{
"questions": [
{
"question": "текст вопроса на английском",
{{
"question": "текст вопроса на изучаемом языке",
"question_ru": "перевод вопроса на русский",
"options": ["вариант A", "вариант B", "вариант C", "вариант D"],
"correct": 0,
"level": "A1"
}
"level": "{level_example}"
}}
]
}
}}
Требования:
- Вопросы 1-2: уровень A1 (базовый)
- Вопросы 3-4: уровень A2-B1 (элементарный-средний)
- Вопросы 5-6: уровень B2-C1 (продвинутый)
- Вопрос 7: уровень C2 (профессиональный)
{levels_req}
- Каждый вопрос с 4 вариантами ответа
- correct - индекс правильного ответа (0-3)
- Вопросы на грамматику, лексику и понимание"""
try:
logger.info(f"[GPT Request] generate_level_test: generating 7 questions")
logger.info(f"[GPT Request] generate_level_test: generating 7 questions for {learning_language}")
system_msg = f"Ты - эксперт по тестированию уровня {language_name} языка. Создавай объективные тесты."
messages = [
{"role": "system", "content": "Ты - эксперт по тестированию уровня английского языка. Создавай объективные тесты."},
{"role": "system", "content": system_msg},
{"role": "user", "content": prompt}
]
@@ -540,57 +560,117 @@ User: {user_message}
except Exception as e:
logger.error(f"[GPT Error] generate_level_test: {type(e).__name__}: {str(e)}, using fallback questions")
# Fallback с базовыми вопросами
return [
{
"question": "What is your name?",
"question_ru": "Как тебя зовут?",
"options": ["My name is", "I am name", "Name my is", "Is name my"],
"correct": 0,
"level": "A1"
},
{
"question": "I ___ to school every day.",
"question_ru": "Я ___ в школу каждый день.",
"options": ["go", "goes", "going", "went"],
"correct": 0,
"level": "A1"
},
{
"question": "She ___ been to Paris twice.",
"question_ru": "Она ___ в Париже дважды.",
"options": ["have", "has", "had", "having"],
"correct": 1,
"level": "A2"
},
{
"question": "If I ___ rich, I would travel the world.",
"question_ru": "Если бы я был богат, я бы путешествовал по миру.",
"options": ["am", "was", "were", "be"],
"correct": 2,
"level": "B1"
},
{
"question": "The project ___ by next Monday.",
"question_ru": "Проект ___ к следующему понедельнику.",
"options": ["will complete", "will be completed", "completes", "is completing"],
"correct": 1,
"level": "B2"
},
{
"question": "Had I known about the meeting, I ___ attended.",
"question_ru": "Если бы я знал о встрече, я бы посетил.",
"options": ["would have", "will have", "would", "will"],
"correct": 0,
"level": "C1"
},
{
"question": "The nuances of his argument were so ___ that few could grasp them.",
"question_ru": "Нюансы его аргумента были настолько ___, что немногие могли их понять.",
"options": ["subtle", "obvious", "simple", "clear"],
"correct": 0,
"level": "C2"
}
]
if learning_language == "ja":
return self._get_jlpt_fallback_questions()
return self._get_cefr_fallback_questions()
def _get_cefr_fallback_questions(self) -> List[Dict]:
"""Fallback вопросы для CEFR (английский и европейские языки)"""
return [
{
"question": "What is your name?",
"question_ru": "Как тебя зовут?",
"options": ["My name is", "I am name", "Name my is", "Is name my"],
"correct": 0,
"level": "A1"
},
{
"question": "I ___ to school every day.",
"question_ru": "Я ___ в школу каждый день.",
"options": ["go", "goes", "going", "went"],
"correct": 0,
"level": "A1"
},
{
"question": "She ___ been to Paris twice.",
"question_ru": "Она ___ в Париже дважды.",
"options": ["have", "has", "had", "having"],
"correct": 1,
"level": "A2"
},
{
"question": "If I ___ rich, I would travel the world.",
"question_ru": "Если бы я был богат, я бы путешествовал по миру.",
"options": ["am", "was", "were", "be"],
"correct": 2,
"level": "B1"
},
{
"question": "The project ___ by next Monday.",
"question_ru": "Проект ___ к следующему понедельнику.",
"options": ["will complete", "will be completed", "completes", "is completing"],
"correct": 1,
"level": "B2"
},
{
"question": "Had I known about the meeting, I ___ attended.",
"question_ru": "Если бы я знал о встрече, я бы посетил.",
"options": ["would have", "will have", "would", "will"],
"correct": 0,
"level": "C1"
},
{
"question": "The nuances of his argument were so ___ that few could grasp them.",
"question_ru": "Нюансы его аргумента были настолько ___, что немногие могли их понять.",
"options": ["subtle", "obvious", "simple", "clear"],
"correct": 0,
"level": "C2"
}
]
def _get_jlpt_fallback_questions(self) -> List[Dict]:
"""Fallback вопросы для JLPT (японский)"""
return [
{
"question": "これは ___です。",
"question_ru": "Это ___.",
"options": ["ほん", "本ん", "ぼん", "もと"],
"correct": 0,
"level": "N5"
},
{
"question": "私は毎日学校に___。",
"question_ru": "Я каждый день хожу в школу.",
"options": ["いきます", "いくます", "いきす", "いきました"],
"correct": 0,
"level": "N5"
},
{
"question": "昨日、映画を___から、今日は勉強します。",
"question_ru": "Вчера я посмотрел фильм, поэтому сегодня буду учиться.",
"options": ["見た", "見て", "見る", "見ない"],
"correct": 0,
"level": "N4"
},
{
"question": "この本は読み___です。",
"question_ru": "Эту книгу легко/трудно читать.",
"options": ["やすい", "にくい", "たい", "そう"],
"correct": 0,
"level": "N3"
},
{
"question": "彼の話を聞く___、涙が出てきた。",
"question_ru": "Слушая его рассказ, у меня потекли слёзы.",
"options": ["につれて", "にしたがって", "とともに", "うちに"],
"correct": 0,
"level": "N2"
},
{
"question": "その計画は実現不可能と___。",
"question_ru": "Этот план считается невыполнимым.",
"options": ["言わざるを得ない", "言うまでもない", "言いかねない", "言うに及ばない"],
"correct": 0,
"level": "N2"
},
{
"question": "彼の行動は___に堪えない。",
"question_ru": "Его поведение невозможно понять/вынести.",
"options": ["理解", "批判", "説明", "弁解"],
"correct": 0,
"level": "N1"
}
]
# Глобальный экземпляр сервиса