feat: мини-истории, слово дня, меню практики

- Добавлены мини-истории для чтения с выбором жанра и вопросами
- Кнопка показа/скрытия перевода истории
- Количество вопросов берётся из настроек пользователя
- Слово дня генерируется глобально в 00:00 UTC
- Кнопка "Практика" открывает меню выбора режима
- Убран автоматический create_all при запуске (только миграции)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-09 15:05:38 +03:00
parent 69c651c031
commit f38ff2f18e
22 changed files with 3131 additions and 77 deletions

View File

@@ -139,3 +139,52 @@ class AIModel(Base):
display_name: Mapped[str] = mapped_column(String(100), nullable=False) # Название для отображения
is_active: Mapped[bool] = mapped_column(Boolean, default=False) # Только одна модель активна
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class StoryGenre(str, enum.Enum):
"""Жанры мини-историй"""
dialogue = "dialogue" # 🗣 Диалоги
news = "news" # 📰 Новости
story = "story" # 🎭 Истории
letter = "letter" # 📧 Письма
recipe = "recipe" # 🍳 Рецепты
class MiniStory(Base):
"""Модель мини-истории для чтения"""
__tablename__ = "mini_stories"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
content: Mapped[str] = mapped_column(String(5000), nullable=False) # Текст истории
translation: Mapped[Optional[str]] = mapped_column(String(5000), nullable=True) # Перевод истории
genre: Mapped[StoryGenre] = mapped_column(SQLEnum(StoryGenre), nullable=False)
learning_lang: Mapped[str] = mapped_column(String(5), nullable=False) # en/ja
level: Mapped[str] = mapped_column(String(5), nullable=False) # A1-C2 или N5-N1
word_count: Mapped[int] = mapped_column(Integer, default=0) # Количество слов
vocabulary: Mapped[Optional[dict]] = mapped_column(JSON) # [{word, translation, transcription}]
questions: Mapped[Optional[dict]] = mapped_column(JSON) # [{question, options[], correct}]
is_completed: Mapped[bool] = mapped_column(Boolean, default=False)
correct_answers: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class WordOfDay(Base):
"""Модель слова дня (глобальная для всех пользователей по уровню)"""
__tablename__ = "word_of_day"
__table_args__ = (
UniqueConstraint("date", "learning_lang", "level", name="uq_wod_date_lang_level"),
)
id: Mapped[int] = mapped_column(primary_key=True)
word: Mapped[str] = mapped_column(String(255), nullable=False)
transcription: Mapped[Optional[str]] = mapped_column(String(255))
translation: Mapped[str] = mapped_column(String(500), nullable=False)
examples: Mapped[Optional[dict]] = mapped_column(JSON) # [{sentence, translation}]
synonyms: Mapped[Optional[str]] = mapped_column(String(500)) # Синонимы через запятую
etymology: Mapped[Optional[str]] = mapped_column(String(500)) # Этимология/интересный факт
learning_lang: Mapped[str] = mapped_column(String(5), nullable=False, index=True) # en/ja
level: Mapped[str] = mapped_column(String(5), nullable=False, index=True) # A1-C2 или N5-N1
date: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True) # Дата слова (только дата, без времени)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)