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:
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# Глобальный экземпляр сервиса
|
||||
|
||||
@@ -2,6 +2,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from database.models import User, LanguageLevel
|
||||
from typing import Optional
|
||||
from utils.levels import set_user_level_for_language, get_default_level
|
||||
|
||||
|
||||
class UserService:
|
||||
@@ -59,14 +60,15 @@ class UserService:
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def update_user_level(session: AsyncSession, user_id: int, level: LanguageLevel):
|
||||
async def update_user_level(session: AsyncSession, user_id: int, level: str, language: str = None):
|
||||
"""
|
||||
Обновить уровень английского пользователя
|
||||
Обновить уровень пользователя для языка изучения.
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
level: Новый уровень
|
||||
level: Новый уровень (строка, например "B1" или "N4")
|
||||
language: Язык (если None, берётся learning_language пользователя)
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
@@ -74,7 +76,11 @@ class UserService:
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user:
|
||||
user.level = level
|
||||
# Сохраняем в JSON для всех языков
|
||||
set_user_level_for_language(user, level, language)
|
||||
# Для обратной совместимости обновляем старое поле level (только для CEFR)
|
||||
if level in ["A1", "A2", "B1", "B2", "C1", "C2"]:
|
||||
user.level = LanguageLevel[level]
|
||||
await session.commit()
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user