feat: restructure menu and add file import
- Consolidate "Add word" menu with submenu (Manual, Thematic, Import) - Add file import support (.txt, .md) with AI batch translation - Add vocabulary pagination with navigation buttons - Add "Add word" button in tasks for new words mode - Fix undefined variables bug in vocabulary confirm handler - Add localization keys for add_menu in ru/en/ja 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -111,6 +111,92 @@ class AIService:
|
||||
"difficulty": "A1"
|
||||
}
|
||||
|
||||
async def translate_words_batch(
|
||||
self,
|
||||
words: List[str],
|
||||
source_lang: str = "en",
|
||||
translation_lang: str = "ru"
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Перевести список слов пакетно
|
||||
|
||||
Args:
|
||||
words: Список слов для перевода
|
||||
source_lang: Язык исходных слов (ISO2)
|
||||
translation_lang: Язык перевода (ISO2)
|
||||
|
||||
Returns:
|
||||
List[Dict] с переводами, транскрипциями
|
||||
"""
|
||||
if not words:
|
||||
return []
|
||||
|
||||
words_list = "\n".join(f"- {w}" for w in words[:50]) # Максимум 50 слов за раз
|
||||
|
||||
# Добавляем инструкцию для фуриганы если японский
|
||||
furigana_instruction = ""
|
||||
if source_lang == "ja":
|
||||
furigana_instruction = '\n "reading": "чтение хираганой (только для кандзи)",'
|
||||
|
||||
prompt = f"""Переведи следующие слова/фразы с языка {source_lang} на {translation_lang}:
|
||||
|
||||
{words_list}
|
||||
|
||||
Верни ответ строго в формате JSON массива:
|
||||
[
|
||||
{{
|
||||
"word": "исходное слово",
|
||||
"translation": "перевод",
|
||||
"transcription": "транскрипция (IPA или ромадзи для японского)",{furigana_instruction}
|
||||
}},
|
||||
...
|
||||
]
|
||||
|
||||
Важно:
|
||||
- Верни только JSON массив, без дополнительного текста
|
||||
- Сохрани порядок слов как в исходном списке
|
||||
- Для каждого слова укажи точный перевод и транскрипцию"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] translate_words_batch: {len(words)} words, {source_lang} -> {translation_lang}")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - помощник для изучения языков. Отвечай только в формате JSON."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.3)
|
||||
|
||||
import json
|
||||
content = response_data['choices'][0]['message']['content']
|
||||
# Убираем markdown обёртку если есть
|
||||
if content.startswith('```'):
|
||||
content = content.split('\n', 1)[1] if '\n' in content else content[3:]
|
||||
if content.endswith('```'):
|
||||
content = content[:-3]
|
||||
content = content.strip()
|
||||
|
||||
result = json.loads(content)
|
||||
|
||||
# Если вернулся dict с ключом типа "words" или "translations" — извлекаем список
|
||||
if isinstance(result, dict):
|
||||
for key in ['words', 'translations', 'result', 'data']:
|
||||
if key in result and isinstance(result[key], list):
|
||||
result = result[key]
|
||||
break
|
||||
|
||||
if not isinstance(result, list):
|
||||
logger.warning(f"[GPT Warning] translate_words_batch: unexpected format, got {type(result)}")
|
||||
return [{"word": w, "translation": "", "transcription": ""} for w in words]
|
||||
|
||||
logger.info(f"[GPT Response] translate_words_batch: success, got {len(result)} translations")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] translate_words_batch: {type(e).__name__}: {str(e)}")
|
||||
# Возвращаем слова без перевода в случае ошибки
|
||||
return [{"word": w, "translation": "", "transcription": ""} for w in words]
|
||||
|
||||
async def check_answer(self, question: str, correct_answer: str, user_answer: str) -> Dict:
|
||||
"""
|
||||
Проверить ответ пользователя с помощью ИИ
|
||||
|
||||
@@ -90,14 +90,21 @@ class VocabularyService:
|
||||
return [w for w in words if not VocabularyService._is_japanese(w.word_original)]
|
||||
|
||||
@staticmethod
|
||||
async def get_user_words(session: AsyncSession, user_id: int, limit: int = 50, learning_lang: Optional[str] = None) -> List[Vocabulary]:
|
||||
async def get_user_words(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
learning_lang: Optional[str] = None
|
||||
) -> List[Vocabulary]:
|
||||
"""
|
||||
Получить все слова пользователя
|
||||
Получить слова пользователя с пагинацией
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
limit: Максимальное количество слов
|
||||
offset: Смещение для пагинации
|
||||
|
||||
Returns:
|
||||
Список слов пользователя
|
||||
@@ -109,7 +116,7 @@ class VocabularyService:
|
||||
)
|
||||
words = list(result.scalars().all())
|
||||
words = VocabularyService._filter_by_learning_lang(words, learning_lang)
|
||||
return words[:limit]
|
||||
return words[offset:offset + limit]
|
||||
|
||||
@staticmethod
|
||||
async def get_words_count(session: AsyncSession, user_id: int, learning_lang: Optional[str] = None) -> int:
|
||||
|
||||
Reference in New Issue
Block a user