From 37570e9c58f5da0c35a3c7d4155e25b4d26bf1a3 Mon Sep 17 00:00:00 2001 From: wilson Date: Tue, 2 Jun 2026 21:02:50 +0100 Subject: [PATCH] monster commit: Make changes to the "summary article" generation flow --- .gitignore | 1 + api/alembic/env.py | 14 +- .../20260531_0020_add_article_tables.py | 77 ++++++ api/app/domain/ai_prompts/__init__.py | 0 .../ai_prompts/summarise_article_ai_prompt.py | 18 ++ api/app/domain/models/article.py | 36 +++ api/app/domain/services/adventure_service.py | 1 - api/app/domain/services/article_service.py | 73 ++++++ api/app/domain/services/summarise_service.py | 162 +++++------- .../outbound/anthropic/anthropic_client.py | 75 ++++-- .../postgres/entities/article_entities.py | 64 +++++ .../repositories/article_repository.py | 206 +++++++++++++++ api/app/routers/api/articles.py | 82 ++++++ api/app/routers/api/generation.py | 26 +- api/app/routers/api/jobs.py | 42 +-- api/app/routers/api/main.py | 29 +- api/app/routers/bff/articles.py | 86 +++--- api/app/tasks/__init__.py | 6 +- api/app/tasks/app.py | 2 +- ...summarise.py => create_summary_article.py} | 27 +- api/conftest.py | 66 +++++ api/pyproject.toml | 7 + api/tests/__init__.py | 0 api/tests/test_article_router.py | 20 ++ api/uv.lock | 10 +- docker-compose-dev.yml | 1 + docker-compose-prod.yml | 2 + docker-compose.test.yml | 5 + frontend/docs/openapi.json | 2 +- frontend/src/client/index.ts | 2 +- frontend/src/client/sdk.gen.ts | 4 +- frontend/src/client/types.gen.ts | 92 ++----- frontend/src/routes/app/articles/+page.svelte | 24 +- .../app/articles/[article_id]/+page.svelte | 248 +++--------------- .../app/generate/summary/+page.server.ts | 34 ++- .../routes/app/generate/summary/+page.svelte | 45 ++-- 36 files changed, 974 insertions(+), 615 deletions(-) create mode 100644 api/alembic/versions/20260531_0020_add_article_tables.py create mode 100644 api/app/domain/ai_prompts/__init__.py create mode 100644 api/app/domain/ai_prompts/summarise_article_ai_prompt.py create mode 100644 api/app/domain/models/article.py create mode 100644 api/app/domain/services/article_service.py create mode 100644 api/app/outbound/postgres/entities/article_entities.py create mode 100644 api/app/outbound/postgres/repositories/article_repository.py create mode 100644 api/app/routers/api/articles.py rename api/app/tasks/{summarise.py => create_summary_article.py} (64%) create mode 100644 api/conftest.py create mode 100644 api/tests/__init__.py create mode 100644 api/tests/test_article_router.py diff --git a/.gitignore b/.gitignore index 39a84f6..6bfb23e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ todo.md .env .env.prod +.env.test .codegraph /Language*Learning*API/ diff --git a/api/alembic/env.py b/api/alembic/env.py index 9a5062c..bafad26 100644 --- a/api/alembic/env.py +++ b/api/alembic/env.py @@ -1,18 +1,18 @@ import asyncio from logging.config import fileConfig -from alembic import context from sqlalchemy import pool from sqlalchemy.ext.asyncio import async_engine_from_config -from app.config import settings -from app.outbound.postgres.database import Base - -import app.outbound.postgres.entities.summarise_job_entity -import app.outbound.postgres.entities.user_entity +import app.outbound.postgres.entities.adventure_entities +import app.outbound.postgres.entities.article_entities import app.outbound.postgres.entities.dictionary_entities import app.outbound.postgres.entities.pack_entities -import app.outbound.postgres.entities.adventure_entities +import app.outbound.postgres.entities.summarise_job_entity +import app.outbound.postgres.entities.user_entity +from alembic import context +from app.config import settings +from app.outbound.postgres.database import Base config = context.config config.set_main_option("sqlalchemy.url", settings.database_url) diff --git a/api/alembic/versions/20260531_0020_add_article_tables.py b/api/alembic/versions/20260531_0020_add_article_tables.py new file mode 100644 index 0000000..b7c2850 --- /dev/null +++ b/api/alembic/versions/20260531_0020_add_article_tables.py @@ -0,0 +1,77 @@ +"""add article tables + +Revision ID: 0020 +Revises: 0019 +Create Date: 2026-05-31 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op + +revision: str = "0020" +down_revision: Union[str, None] = "0019" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "article", + sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column("article_type", sa.Text(), nullable=False), + sa.Column("language", sa.Text(), nullable=False), + sa.Column("target_complexity", sa.Text(), nullable=False), + sa.Column("title", sa.Text(), nullable=False), + sa.Column("text", sa.Text(), nullable=False), + sa.Column("text_linguistic_data", postgresql.JSONB(), nullable=True), + sa.Column("audio_key", sa.Text(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.func.now(), + ), + sa.Column("published_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + ) + + op.create_table( + "article_ownership", + sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column( + "article_id", + postgresql.UUID(as_uuid=True), + sa.ForeignKey("article.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("ownership_role", sa.Text(), nullable=False), + sa.Column( + "user_id", + postgresql.UUID(as_uuid=True), + sa.ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.func.now(), + ), + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + ) + op.create_index( + "ix_article_ownership_article_id", "article_ownership", ["article_id"] + ) + op.create_index("ix_article_ownership_user_id", "article_ownership", ["user_id"]) + + +def downgrade() -> None: + op.drop_index("ix_article_ownership_user_id", table_name="article_ownership") + op.drop_index("ix_article_ownership_article_id", table_name="article_ownership") + op.drop_table("article_ownership") + op.drop_table("article") diff --git a/api/app/domain/ai_prompts/__init__.py b/api/app/domain/ai_prompts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/app/domain/ai_prompts/summarise_article_ai_prompt.py b/api/app/domain/ai_prompts/summarise_article_ai_prompt.py new file mode 100644 index 0000000..d48c2a4 --- /dev/null +++ b/api/app/domain/ai_prompts/summarise_article_ai_prompt.py @@ -0,0 +1,18 @@ +def summarise_article_system_prompt( + to_language: str = "french", + complexity_level: str = "B1", + length_preference: str = "300 words", +) -> str: + return ( + f"You are a {to_language} language learning content creator, tutoring someone at {complexity_level} proficiency level on the CEFR scale.\n" + f"Generate level-appropriate content from a source.\n" + f"Your response will:\n" + f"- Start with a level-one markdown title .\n" + f"- Then contain only the article, in {to_language}, as plain-text. \n" + f"- Separate each paragraph (and the title) with two new line characters.\n" + f"- Speak directly to the reader in a semi-formal, modern media tone.\n" + f"- Occasionally, where natural, include idiomatic expressions appropriate to {complexity_level} level.\n" + f"- Vary gramatical tenses, but naturally — do not restrict the piece to a single tense.\n" + f"- Be around {length_preference} long.\n" + f"- Be inspired by the content, but not the tone, of the source material." + ) diff --git a/api/app/domain/models/article.py b/api/app/domain/models/article.py new file mode 100644 index 0000000..60a9b03 --- /dev/null +++ b/api/app/domain/models/article.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from datetime import datetime +from enum import Enum + + +class ArticleTypeEnum(str, Enum): + summary = "summary" # take the input text, and summarise it + + +@dataclass +class Article: + id: str + article_type: ArticleTypeEnum + language: str # e.g. "fr" + target_complexity: str # e.g. "B1" + title: str + text: str + text_linguistic_data: dict | None + audio_key: str | None + created_at: datetime + published_at: datetime | None + deleted_at: datetime | None + + +class ArticleOwnershipRoleEnum(str, Enum): + owner = "owner" # Person for who the Article was created + + +@dataclass +class ArticleOwnership: + id: str + article_id: str + ownership_role: ArticleOwnershipRoleEnum + user_id: str + created_at: datetime + deleted_at: datetime | None diff --git a/api/app/domain/services/adventure_service.py b/api/app/domain/services/adventure_service.py index 07c1995..7c85464 100644 --- a/api/app/domain/services/adventure_service.py +++ b/api/app/domain/services/adventure_service.py @@ -14,7 +14,6 @@ from ...outbound.anthropic.adventure_prompts import ( parse_entry_response, parse_title_response, ) -from ...outbound.anthropic.anthropic_client import AnthropicClient from ...outbound.deepl.deepl_client import DeepLClient from ...outbound.gemini.gemini_client import GeminiClient from ...outbound.postgres.repositories.adventure_repository import ( diff --git a/api/app/domain/services/article_service.py b/api/app/domain/services/article_service.py new file mode 100644 index 0000000..c05ca56 --- /dev/null +++ b/api/app/domain/services/article_service.py @@ -0,0 +1,73 @@ +import logging +from uuid import UUID, uuid4 + +from app.domain.models.article import Article, ArticleOwnershipRoleEnum, ArticleTypeEnum +from app.outbound.postgres.repositories.article_repository import ( + ArticleOwnershipRepository, + ArticleRepository, +) + +logger = logging.getLogger(__name__) + + +class ArticleService: + def __init__( + self, + article_repository: ArticleRepository, + article_ownership_repository: ArticleOwnershipRepository, + ) -> None: + self.article_repository = article_repository + self.article_ownership_repository = article_ownership_repository + return + + async def create_article_as_user( + self, + article_type: ArticleTypeEnum, + language: str, + target_complexity: str, + title: str, + text: str, + user_id: str, + ) -> Article: + article = await self.article_repository.create( + article_type=article_type, + language=language, + target_complexity=target_complexity, + title=title, + text=text, + ) + + await self.article_ownership_repository.create( + article_id=UUID(article.id), + ownership_role=ArticleOwnershipRoleEnum.owner, + user_id=UUID(user_id), + ) + + return article + + async def get_articles_for_user(self, user_id: str) -> list[Article]: + articles = await self.article_repository.get_non_deleted_articles_for_owner( + UUID(user_id) + ) + return articles + + async def get_article_as_user( + self, article_id: str, user_id: str + ) -> Article | None: + aid = UUID(article_id) + article = await self.article_repository.get_by_id(aid) + if article is None: + logger.info(f"Article with id {article_id} not found") + return None + + ownerships = await self.article_ownership_repository.get_by_article_id(aid) + print(f"Current user: {user_id}") + for o in ownerships: + print(o) + if not any(ownership.user_id == user_id for ownership in ownerships): + logger.info( + f"User with id {user_id} does not have access to article with id {article_id}" + ) + return None + + return article diff --git a/api/app/domain/services/summarise_service.py b/api/app/domain/services/summarise_service.py index 52f8579..422cf54 100644 --- a/api/app/domain/services/summarise_service.py +++ b/api/app/domain/services/summarise_service.py @@ -1,55 +1,23 @@ -import asyncio -import random +import logging import re import uuid -from typing import Any, Callable, Coroutine -import anthropic +from opentelemetry.trace import get_tracer from sqlalchemy.ext.asyncio import AsyncSession -from ...outbound.postgres.repositories import summarise_job_repository -from ...outbound.postgres.repositories.translated_article_repository import TranslatedArticleRepository +from app.outbound.postgres.repositories.article_repository import ArticleRepository + +from ...languages import SUPPORTED_LANGUAGES from ...outbound.anthropic.anthropic_client import AnthropicClient from ...outbound.deepgram.deepgram_client import LocalDeepgramClient from ...outbound.deepl.deepl_client import DeepLClient from ...outbound.gemini.gemini_client import GeminiClient from ...outbound.spacy.spacy_client import SpacyClient from ...outbound.storage_client import get_storage_client -from ...languages import SUPPORTED_LANGUAGES +logger = logging.getLogger(__name__) - -_ANTHROPIC_RETRYABLE = ( - anthropic.RateLimitError, - anthropic.InternalServerError, - anthropic.APITimeoutError, - anthropic.APIConnectionError, -) -_MAX_RETRIES = 4 -_BASE_DELAY = 1.0 -_MAX_DELAY = 60.0 - - -async def _anthropic_with_backoff( - coro_fn: Callable[..., Coroutine[Any, Any, Any]], - *args: Any, - **kwargs: Any, -) -> Any: - for attempt in range(_MAX_RETRIES + 1): - try: - return await coro_fn(*args, **kwargs) - except _ANTHROPIC_RETRYABLE as exc: - if attempt == _MAX_RETRIES: - raise - retry_after: float | None = None - if isinstance(exc, anthropic.RateLimitError): - raw = exc.response.headers.get("retry-after") - if raw is not None: - retry_after = float(raw) - if retry_after is None: - retry_after = min(_BASE_DELAY * (2 ** attempt), _MAX_DELAY) - jittered = retry_after * (0.8 + random.random() * 0.4) - await asyncio.sleep(jittered) +tracer = get_tracer(__name__) class SummariseService: @@ -60,90 +28,80 @@ class SummariseService: deepl_client: DeepLClient, gemini_client: GeminiClient, spacy_client: SpacyClient, + article_repository: ArticleRepository, ) -> None: self.anthropic_client = anthropic_client self.deepgram_client = deepgram_client self.deepl_client = deepl_client self.gemini_client = gemini_client self.spacy_client = spacy_client + self.article_repository = article_repository - def _first_heading(self, md: str) -> str | None: - m = re.search(r'^#{1,2}\s+(.+)', md, re.MULTILINE) - return m.group(1).strip() if m else None - def _split_title_and_body(self, text: str) -> tuple[str, str]: - """Splits the text into a title (first heading) and body (the rest).""" - title = self._first_heading(text) or "" - body = text[len(title):].lstrip() if title else text - if title == "": - title = "Untitled Article" - + lines = text.splitlines() + + if not lines: + return "", "" + + title = lines[0].lstrip("#").strip() + body = "\n".join(lines[1:]).strip() return title, body + - async def run( + async def summarise_article( self, - db: AsyncSession, - job_id: uuid.UUID, article_id: uuid.UUID, - source_language: str, target_language: str, complexity_level: str, - input_texts: list[str], + input_text: str, ) -> None: - article_repo = TranslatedArticleRepository(db) - job = await summarise_job_repository.get_by_id(db, job_id) - await summarise_job_repository.mark_processing(db, job) + print(f"Summarising article {article_id} with target language {target_language} and complexity level {complexity_level}...") + with tracer.start_as_current_span("summarise_article"): + try: + with tracer.start_as_current_span("generate_title_and_text"): + language_name = SUPPORTED_LANGUAGES[target_language] - try: - language_name = SUPPORTED_LANGUAGES[target_language] - source_material = "\n\n".join(input_texts[:3]) + generated_text = await AnthropicClient.retry( + self.anthropic_client.create_summary_article, + content_to_summarise=input_text, + complexity_level=complexity_level, + to_language=language_name, + length_preference="200-400 words", + ) - generated_text = await _anthropic_with_backoff( - self.anthropic_client.generate_summary_text, - content_to_summarise=source_material, - complexity_level=complexity_level, - from_language=language_name, - to_language=language_name, - length_preference="200-400 words", - ) - - generated_title, generated_text_without_title = self._split_title_and_body(generated_text) + if generated_text is None: + print(f"Text generated to summarise article {article_id}...") + raise - await article_repo.update_content( - article_id, - target_title=generated_title, - target_body=generated_text_without_title, - source_title="", - source_body="", - ) - - translated_text = await self.deepl_client.translate(generated_text, source_language) + generated_title, generated_text_without_title = ( + self._split_title_and_body(generated_text) + ) - translated_title, translated_text_without_title = self._split_title_and_body(translated_text) + await self.article_repository.update_title_and_text( + article_id, generated_title, generated_text_without_title + ) - await article_repo.update_content( - article_id, - target_title=generated_title, - target_body=generated_text_without_title, - source_title=translated_title, - source_body=translated_text_without_title, - ) + with tracer.start_as_current_span("generate_linguistic_data"): + text_linguistic_data = self.spacy_client.get_parts_of_speech( + generated_text_without_title, target_language + ) - target_pos_data = self.spacy_client.get_parts_of_speech(generated_text_without_title, target_language) - source_pos_data = self.spacy_client.get_parts_of_speech(translated_text_without_title, source_language) + await self.article_repository.update_linguistic_data( + article_id, text_linguistic_data + ) - await article_repo.update_pos(article_id, target_pos_data, source_pos_data) + with tracer.start_as_current_span("generate_voice"): + voice = self.gemini_client.get_voice_by_language(target_language) + wav_bytes = await self.gemini_client.generate_audio( + generated_text, voice + ) + audio_key = f"audio/{article_id}.wav" + get_storage_client().upload(audio_key, wav_bytes) - voice = self.gemini_client.get_voice_by_language(target_language) - wav_bytes = await self.gemini_client.generate_audio(generated_text, voice) - audio_key = f"audio/{job_id}.wav" - get_storage_client().upload(audio_key, wav_bytes) + await self.article_repository.update_audio_key( + article_id, audio_key + ) - transcript = await self.deepgram_client.transcribe_bytes(wav_bytes, target_language) - - await article_repo.update_audio(article_id, audio_key, transcript) - - await summarise_job_repository.mark_succeeded(db, job) - - except Exception as exc: - await summarise_job_repository.mark_failed(db, job, str(exc)) + except Exception as exc: + print(f"Failed to summarise an article: {exc}") + raise exc diff --git a/api/app/outbound/anthropic/anthropic_client.py b/api/app/outbound/anthropic/anthropic_client.py index 948c0f3..b280db1 100644 --- a/api/app/outbound/anthropic/anthropic_client.py +++ b/api/app/outbound/anthropic/anthropic_client.py @@ -1,9 +1,24 @@ import asyncio +from random import random +from typing import Any, Callable, Coroutine import anthropic +from app.domain.ai_prompts.summarise_article_ai_prompt import ( + summarise_article_system_prompt, +) from app.domain.models.gen_ai import GenAiChatMessage +_ANTHROPIC_RETRYABLE = ( + anthropic.RateLimitError, + anthropic.InternalServerError, + anthropic.APITimeoutError, + anthropic.APIConnectionError, +) +_MAX_RETRIES = 4 +_BASE_DELAY = 1.0 +_MAX_DELAY = 60.0 + class AnthropicClient: def __init__(self, api_key: str): @@ -13,27 +28,33 @@ class AnthropicClient: def new(cls, api_key: str) -> "AnthropicClient": return cls(api_key) - def _create_summarise_text_system_prompt( - self, - complexity_level: str, - from_language: str, - to_language: str, - length_preference="200-400 words", - ) -> str: - return ( - f"You are a language learning content creator.\n" - f"You generate original, level-appropriate content from a source.\n" - f"The content will be spoken aloud in {to_language}, write it accordingly.\n" - f"You will provide content in {to_language} at {complexity_level} proficiency level on the CEFR scale.\n" - f"The text you generate will:\n" - f"- Contain ONLY the generated summary text in {to_language}.\n" - f"- Speak directly to the reader/listener, adopting the tone and style of a semi-formal news reporter or podcaster.\n" - f"- Occasionally, where natural, include idiomatic expressions appropriate to {complexity_level} level.\n" - f"- Vary tense usage naturally — do not restrict the piece to a single tense.\n" - f"- Contain only plain text. The piece should start with a title prefaced like a level-1 markdown title (#), but all other text should be plain. \n" - f"- Be around {length_preference} long.\n" - f"- Be inspired by the content, but not the tone, of the source material." - ) + @classmethod + async def retry( + cls, + callable_function: Callable[..., Coroutine[Any, Any, Any]], + *args: Any, + **kwargs: Any, + ): + for attempt in range(_MAX_RETRIES + 1): + try: + return await callable_function(*args, **kwargs) + except _ANTHROPIC_RETRYABLE as exception: + if attempt == _MAX_RETRIES: + raise + + retry_after: float | None = None + + if isinstance(exception, anthropic.RateLimitError): + raw = exception.response.header.get("retry-after") + if raw is not None: + retry_after = float(raw) + + if retry_after is None: + retry_after = min(_BASE_DELAY * (2**attempt), _MAX_DELAY) + + jittered = retry_after * (0.8 * random.random() * 0.4) + + await asyncio.sleep(jittered) def _create_prompt_summarise_text( self, @@ -79,24 +100,24 @@ class AnthropicClient: return await asyncio.to_thread(_call) - async def generate_summary_text( + async def create_summary_article( self, content_to_summarise: str, complexity_level: str, - from_language: str, to_language: str, length_preference="200-400 words", ) -> str: - """Generate text using Anthropic.""" + """ + Generate text, and title, for a summary article using Anthropic. + """ def _call() -> str: message = self._client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, - system=self._create_summarise_text_system_prompt( - complexity_level=complexity_level, - from_language=from_language, + system=summarise_article_system_prompt( to_language=to_language, + complexity_level=complexity_level, length_preference=length_preference, ), messages=[ diff --git a/api/app/outbound/postgres/entities/article_entities.py b/api/app/outbound/postgres/entities/article_entities.py new file mode 100644 index 0000000..6f5f859 --- /dev/null +++ b/api/app/outbound/postgres/entities/article_entities.py @@ -0,0 +1,64 @@ +import uuid +from datetime import datetime, timezone + +from sqlalchemy import DateTime, ForeignKey, Text +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.postgresql.json import JSONB +from sqlalchemy.orm import Mapped, mapped_column + +from ..database import Base + + +class ArticleEntity(Base): + __tablename__ = "article" + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 + ) + article_type: Mapped[str] = mapped_column(Text, nullable=False) + language: Mapped[str] = mapped_column(Text, nullable=False) + target_complexity: Mapped[str] = mapped_column(Text, nullable=False) + title: Mapped[str] = mapped_column(Text, nullable=False) + text: Mapped[str] = mapped_column(Text, nullable=False) + text_linguistic_data: Mapped[dict] = mapped_column(JSONB, nullable=True) + audio_key: Mapped[str] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + default=lambda: datetime.now(timezone.utc), + ) + published_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + deleted_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + + +class ArticleOwnershipEntity(Base): + __tablename__ = "article_ownership" + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 + ) + article_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("article.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + ownership_role: Mapped[str] = mapped_column(Text, nullable=False) + user_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + default=lambda: datetime.now(timezone.utc), + ) + deleted_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) diff --git a/api/app/outbound/postgres/repositories/article_repository.py b/api/app/outbound/postgres/repositories/article_repository.py new file mode 100644 index 0000000..4562ac0 --- /dev/null +++ b/api/app/outbound/postgres/repositories/article_repository.py @@ -0,0 +1,206 @@ +import logging +import uuid +from datetime import datetime, timezone +from typing import Protocol + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from ....domain.models.article import ( + Article, + ArticleOwnership, + ArticleOwnershipRoleEnum, + ArticleTypeEnum, +) +from ..entities.article_entities import ArticleEntity, ArticleOwnershipEntity + + +class ArticleRepository(Protocol): + async def create( + self, + article_type: ArticleTypeEnum, + language: str, + target_complexity: str, + title: str, + text: str, + ) -> Article: ... + + async def get_by_id(self, article_id: uuid.UUID) -> Article | None: ... + + async def update_title_and_text( + self, id: uuid.UUID, title: str, text: str + ) -> Article: ... + + async def update_linguistic_data( + self, id: uuid.UUID, linguistic_data: dict + ) -> Article: ... + + async def update_audio_key(self, id: uuid.UUID, audio_key: str) -> Article: ... + + async def get_non_deleted_articles_for_owner(self, owner_id: uuid.UUID) -> list[Article]: ... + + +class ArticleOwnershipRepository(Protocol): + async def create( + self, + article_id: uuid.UUID, + ownership_role: ArticleOwnershipRoleEnum, + user_id: uuid.UUID, + ) -> ArticleOwnership: ... + + async def get_by_article_id( + self, article_id: uuid.UUID + ) -> list[ArticleOwnership]: ... + + +def _article_to_model(entity: ArticleEntity) -> Article: + return Article( + id=str(entity.id), + article_type=ArticleTypeEnum(entity.article_type), + language=entity.language, + target_complexity=entity.target_complexity, + title=entity.title, + text=entity.text, + audio_key=entity.audio_key, + text_linguistic_data=entity.text_linguistic_data, + created_at=entity.created_at, + published_at=entity.published_at, + deleted_at=entity.deleted_at, + ) + + +def _ownership_to_model(entity: ArticleOwnershipEntity) -> ArticleOwnership: + return ArticleOwnership( + id=str(entity.id), + article_id=str(entity.article_id), + ownership_role=ArticleOwnershipRoleEnum(entity.ownership_role), + user_id=str(entity.user_id), + created_at=entity.created_at, + deleted_at=entity.deleted_at, + ) + + +logger = logging.getLogger(__name__) + + +class PostgresArticleRepository: + def __init__(self, db: AsyncSession) -> None: + self.db = db + + async def create( + self, + article_type: ArticleTypeEnum, + language: str, + target_complexity: str, + title: str, + text: str, + ) -> Article: + entity = ArticleEntity( + article_type=article_type.value, + language=language, + target_complexity=target_complexity, + title=title, + text=text, + created_at=datetime.now(timezone.utc), + ) + self.db.add(entity) + await self.db.commit() + await self.db.refresh(entity) + return _article_to_model(entity) + + async def get_by_id(self, article_id: uuid.UUID) -> Article | None: + result = await self.db.execute( + select(ArticleEntity).where(ArticleEntity.id == article_id) + ) + entity = result.scalar_one_or_none() + return _article_to_model(entity) if entity else None + + async def get_non_deleted_articles_for_owner(self, owner_id: uuid.UUID) -> list[Article]: + result = await self.db.execute( + select(ArticleEntity) + .join(ArticleOwnershipEntity, ArticleEntity.id == ArticleOwnershipEntity.article_id) + .where( + ArticleOwnershipEntity.user_id == owner_id, + ArticleOwnershipEntity.deleted_at.is_(None), + ArticleEntity.deleted_at.is_(None), + ArticleOwnershipEntity.ownership_role == ArticleOwnershipRoleEnum.owner.value, + ) + ) + entities = result.scalars().all() + return [_article_to_model(entity) for entity in entities] + + async def update_title_and_text( + self, id: uuid.UUID, title: str, text: str + ) -> Article: + entity = await self.db.execute( + select(ArticleEntity).where(ArticleEntity.id == id) + ) + a = entity.scalar_one_or_none() + + if a is None: + logger.error( + f"update_title_and_text failed, cannot find article with id '{id}'" + ) + raise + + a.title = title + a.text = text + await self.db.commit() + logger.info(f"update_title_and_text for article '{id}' successful") + return _article_to_model(a) + + async def update_linguistic_data( + self, id: uuid.UUID, linguistic_data: dict + ) -> Article: + e = await self.db.execute(select(ArticleEntity).where(ArticleEntity.id == id)) + a = e.scalar_one_or_none() + if a is None: + logger.error( + f"update_linguistic_data failed, cannot find article with id '{id}'" + ) + raise + a.text_linguistic_data = linguistic_data + await self.db.commit() + logger.info(f"update_linguistic_data for article '{id}' successful") + return _article_to_model(a) + + async def update_audio_key(self, id: uuid.UUID, audio_key: str) -> Article: + e = await self.db.execute(select(ArticleEntity).where(ArticleEntity.id == id)) + a = e.scalar_one_or_none() + if a is None: + logger.error(f"update_audio_key failed, cannot find article with id '{id}'") + raise + a.audio_key = audio_key + await self.db.commit() + logger.info(f"update_audio_key for article '{id}' successful") + return _article_to_model(a) + + +class PostgresArticleOwnershipRepository: + def __init__(self, db: AsyncSession) -> None: + self.db = db + + async def create( + self, + article_id: uuid.UUID, + ownership_role: ArticleOwnershipRoleEnum, + user_id: uuid.UUID, + ) -> ArticleOwnership: + entity = ArticleOwnershipEntity( + article_id=article_id, + ownership_role=ownership_role.value, + user_id=user_id, + created_at=datetime.now(timezone.utc), + ) + self.db.add(entity) + await self.db.commit() + await self.db.refresh(entity) + return _ownership_to_model(entity) + + async def get_by_article_id(self, article_id: uuid.UUID) -> list[ArticleOwnership]: + result = await self.db.execute( + select(ArticleOwnershipEntity).where( + ArticleOwnershipEntity.article_id == article_id + ) + ) + return [_ownership_to_model(e) for e in result.scalars().all()] diff --git a/api/app/routers/api/articles.py b/api/app/routers/api/articles.py new file mode 100644 index 0000000..b21c2f7 --- /dev/null +++ b/api/app/routers/api/articles.py @@ -0,0 +1,82 @@ +import uuid +from enum import Enum + +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from sqlalchemy.ext.asyncio.session import AsyncSession +from starlette.status import HTTP_201_CREATED + +from app.auth import verify_token +from app.domain.models.article import Article, ArticleTypeEnum +from app.domain.services.article_service import ArticleService +from app.outbound.postgres.database import get_db +from app.outbound.postgres.repositories.article_repository import ( + PostgresArticleOwnershipRepository, + PostgresArticleRepository, +) +from app.tasks.create_summary_article import create_summary_article + +router = APIRouter(prefix="/articles", tags=["adventures"]) + + +def _make_article_service(db) -> ArticleService: + return ArticleService( + article_repository=PostgresArticleRepository(db), + article_ownership_repository=PostgresArticleOwnershipRepository(db), + ) + + +class CreateArticleBody(BaseModel): + article_type: ArticleTypeEnum + language: str + target_complexity: str + text: str + + +class CreateArticleResponse(BaseModel): + id: str + + +class ArticleItem(BaseModel): + id: str + + +def _to_article_item(article: Article) -> ArticleItem: + return ArticleItem(id=str(article.id)) + + +@router.post("", response_model=CreateArticleResponse, status_code=HTTP_201_CREATED) +async def create_article( + body: CreateArticleBody, + db: AsyncSession = Depends(get_db), + token_data: dict = Depends(verify_token), +) -> CreateArticleResponse: + + service = _make_article_service(db) + + article = await service.create_article_as_user( + article_type=body.article_type, + language=body.language, + target_complexity=body.target_complexity, + text=body.text, + title="", + user_id=token_data["sub"], + ) + + await create_summary_article.defer_async( + article_id=article.id, + target_language=body.language, + complexity_level=body.target_complexity, + input_text=body.text, + ) + + return CreateArticleResponse(id=str(uuid.uuid4())) + + +@router.get("/{article_id}", response_model=ArticleItem, status_code=200) +async def get_article( + article_id: str, + db: AsyncSession = Depends(get_db), + token_data: dict = Depends(verify_token), +) -> ArticleItem: + return ArticleItem(id=article_id) diff --git a/api/app/routers/api/generation.py b/api/app/routers/api/generation.py index 400294b..2dae962 100644 --- a/api/app/routers/api/generation.py +++ b/api/app/routers/api/generation.py @@ -1,5 +1,3 @@ -import uuid - from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession @@ -7,9 +5,10 @@ from sqlalchemy.ext.asyncio import AsyncSession from ...auth import require_admin from ...languages import SUPPORTED_LANGUAGES, SUPPORTED_LEVELS from ...outbound.postgres.database import get_db -from ...outbound.postgres.repositories import summarise_job_repository -from ...outbound.postgres.repositories.translated_article_repository import TranslatedArticleRepository -from ...tasks import summarise_article +from ...outbound.postgres.repositories.translated_article_repository import ( + TranslatedArticleRepository, +) +from ...tasks import create_summary_article router = APIRouter(prefix="/generate", tags=["api"]) @@ -17,12 +16,12 @@ router = APIRouter(prefix="/generate", tags=["api"]) class GenerationRequest(BaseModel): target_language: str complexity_level: str - input_texts: list[str] + text: str source_language: str = "en" class GenerationResponse(BaseModel): - job_id: str + article_id: str @router.post("", response_model=GenerationResponse, status_code=202) @@ -50,19 +49,12 @@ async def create_generation_job( target_complexities=[request.complexity_level], ) - job = await summarise_job_repository.create( - db, - user_id=uuid.UUID(token_data["sub"]), - translated_article_id=uuid.UUID(article.id), - ) - - await summarise_article.defer_async( - job_id=str(job.id), + await create_summary_article.defer_async( article_id=str(article.id), source_language=request.source_language, target_language=request.target_language, complexity_level=request.complexity_level, - input_texts=request.input_texts, + input_text=request.text, ) - return GenerationResponse(job_id=str(job.id)) + return GenerationResponse(article_id=str(article.id)) diff --git a/api/app/routers/api/jobs.py b/api/app/routers/api/jobs.py index b16c3ec..5430954 100644 --- a/api/app/routers/api/jobs.py +++ b/api/app/routers/api/jobs.py @@ -7,7 +7,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from ...auth import require_admin from ...outbound.postgres.database import get_db -from ...outbound.postgres.entities.translated_article_entity import TranslatedArticleEntity +from ...outbound.postgres.entities.translated_article_entity import ( + TranslatedArticleEntity, +) from ...outbound.postgres.repositories import summarise_job_repository from ...tasks import regenerate_audio_for_job @@ -41,7 +43,7 @@ class JobListResponse(BaseModel): @router.get("/", response_model=JobListResponse) async def get_jobs( - db: AsyncSession = Depends(get_db), + db: AsyncSession = Depends(get_db), ) -> JobListResponse: try: jobs = await summarise_job_repository.list_all(db) @@ -73,39 +75,3 @@ async def get_job( completed_at=job.completed_at, error_message=job.error_message, ) - - -@router.post("/{job_id}/regenerate-audio", status_code=202) -async def regenerate_audio( - job_id: str, - db: AsyncSession = Depends(get_db), - token_data: dict = Depends(require_admin), -) -> dict: - try: - uid = uuid.UUID(job_id) - except ValueError: - raise HTTPException(status_code=400, detail="Invalid job ID format") - - job = await summarise_job_repository.get_by_id(db, uid) - if job is None: - raise HTTPException(status_code=404, detail="Job not found") - - if str(job.user_id) != token_data["sub"]: - raise HTTPException(status_code=403, detail="Not authorized to modify this job") - - if job.translated_article_id is None: - raise HTTPException(status_code=400, detail="Job has no associated article") - - article_entity = await db.get(TranslatedArticleEntity, job.translated_article_id) - - if not article_entity or not article_entity.target_body: - raise HTTPException(status_code=400, detail="Job has no generated text to synthesize") - - if article_entity.audio_url: - raise HTTPException(status_code=409, detail="Job already has audio") - - if job.status == "processing": - raise HTTPException(status_code=409, detail="Job is already processing") - - await regenerate_audio_for_job.defer_async(job_id=str(uid)) - return {"job_id": job_id} diff --git a/api/app/routers/api/main.py b/api/app/routers/api/main.py index 52b9794..91a3c21 100644 --- a/api/app/routers/api/main.py +++ b/api/app/routers/api/main.py @@ -1,31 +1,34 @@ +from fastapi import APIRouter + + +from .articles import router as article_router from .account import router as account_router +from .admin.packs import router as admin_packs_router +from .adventures import router as adventures_router from .auth import router as auth_router from .dictionary import router as dictionary_router from .flashcards import router as flashcards_router -from .pos import router as pos_router -from .translate import router as translate_router from .generation import router as generation_router from .jobs import router as jobs_router from .learnable_languages import router as learnable_languages_router -from .vocab import router as vocab_router from .packs import router as packs_router -from .admin.packs import router as admin_packs_router -from .adventures import router as adventures_router - -from fastapi import APIRouter +from .pos import router as pos_router +from .translate import router as translate_router +from .vocab import router as vocab_router api_router = APIRouter(prefix="/api", tags=["api"]) -api_router.include_router(auth_router) api_router.include_router(account_router) +api_router.include_router(admin_packs_router) +api_router.include_router(adventures_router) +api_router.include_router(article_router) +api_router.include_router(auth_router) api_router.include_router(dictionary_router) api_router.include_router(flashcards_router) -api_router.include_router(pos_router) -api_router.include_router(translate_router) api_router.include_router(generation_router) api_router.include_router(jobs_router) api_router.include_router(learnable_languages_router) -api_router.include_router(vocab_router) api_router.include_router(packs_router) -api_router.include_router(admin_packs_router) -api_router.include_router(adventures_router) +api_router.include_router(pos_router) +api_router.include_router(translate_router) +api_router.include_router(vocab_router) diff --git a/api/app/routers/bff/articles.py b/api/app/routers/bff/articles.py index c877775..03251ab 100644 --- a/api/app/routers/bff/articles.py +++ b/api/app/routers/bff/articles.py @@ -1,26 +1,35 @@ -import uuid from datetime import datetime from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession +from app.domain.services.article_service import ArticleService +from app.outbound.postgres.repositories.article_repository import ( + PostgresArticleOwnershipRepository, + PostgresArticleRepository, +) + from ...auth import verify_token from ...outbound.postgres.database import get_db from ...outbound.storage_client import get_storage_client -from ...outbound.postgres.repositories.translated_article_repository import TranslatedArticleRepository router = APIRouter(prefix="/articles", tags=["bff", "articles"]) +def _make_article_service(db) -> ArticleService: + return ArticleService( + article_repository=PostgresArticleRepository(db), + article_ownership_repository=PostgresArticleOwnershipRepository(db), + ) + + class ArticleItem(BaseModel): id: str - published_at: datetime - source_language: str - source_title: str - target_language: str - target_complexities: list[str] - target_title: str + published_at: datetime | None + language: str + title: str + complexity: str class ArticleListResponse(BaseModel): @@ -29,18 +38,13 @@ class ArticleListResponse(BaseModel): class ArticleDetail(BaseModel): id: str - published_at: datetime - source_language: str - source_title: str - source_body: str - source_body_pos: dict - target_language: str - target_complexities: list[str] - target_title: str - target_body: str - target_audio_url: str | None - target_body_pos: dict - target_body_transcript: dict | None + published_at: datetime | None + language: str + complexity: str + title: str + body: str + audio_url: str | None + body_pos: dict | None def _audio_url(key: str | None) -> str | None: @@ -51,21 +55,20 @@ def _audio_url(key: str | None) -> str | None: @router.get("", response_model=ArticleListResponse, status_code=200) async def list_articles( - target_language: str = 'fr', db: AsyncSession = Depends(get_db), - _: dict = Depends(verify_token), + token_data: dict = Depends(verify_token), ) -> ArticleListResponse: - articles = await TranslatedArticleRepository(db).list_complete(target_language=target_language) + service = _make_article_service(db) + user_id = token_data["sub"] + articles = await service.get_articles_for_user(user_id=user_id) return ArticleListResponse( articles=[ ArticleItem( id=a.id, published_at=a.published_at, - source_language=a.source_language, - source_title=a.source_title, - target_language=a.target_language, - target_complexities=a.target_complexities, - target_title=a.target_title, + language=a.language, + title=a.title, + complexity=a.target_complexity, ) for a in articles ] @@ -76,29 +79,22 @@ async def list_articles( async def get_article( article_id: str, db: AsyncSession = Depends(get_db), - _: dict = Depends(verify_token), + token_data: dict = Depends(verify_token), ) -> ArticleDetail: - try: - uid = uuid.UUID(article_id) - except ValueError: - raise HTTPException(status_code=400, detail="Invalid article ID") + uid: str = token_data["sub"] + service = _make_article_service(db) + article = await service.get_article_as_user(article_id, uid) - article = await TranslatedArticleRepository(db).get_complete_by_id(uid) if article is None: raise HTTPException(status_code=404, detail="Article not found") return ArticleDetail( id=article.id, published_at=article.published_at, - source_language=article.source_language, - source_title=article.source_title, - source_body=article.source_body, - source_body_pos=article.source_body_pos, - target_language=article.target_language, - target_complexities=article.target_complexities, - target_title=article.target_title, - target_body=article.target_body, - target_audio_url=_audio_url(article.audio_url), - target_body_pos=article.target_body_pos, - target_body_transcript=article.target_body_transcript, + language=article.language, + complexity=article.target_complexity, + title=article.title, + body=article.text, + body_pos=article.text_linguistic_data, + audio_url=_audio_url(article.audio_key), ) diff --git a/api/app/tasks/__init__.py b/api/app/tasks/__init__.py index 9146c4f..95774a5 100644 --- a/api/app/tasks/__init__.py +++ b/api/app/tasks/__init__.py @@ -1,11 +1,11 @@ -from .app import procrastinate_app from .adventure import generate_adventure_entry -from .summarise import summarise_article +from .app import procrastinate_app +from .create_summary_article import create_summary_article from .regenerate_audio import regenerate_audio_for_job __all__ = [ "procrastinate_app", "generate_adventure_entry", - "summarise_article", + "create_summary_article", "regenerate_audio_for_job", ] diff --git a/api/app/tasks/app.py b/api/app/tasks/app.py index e42e11c..7ab03a1 100644 --- a/api/app/tasks/app.py +++ b/api/app/tasks/app.py @@ -8,7 +8,7 @@ procrastinate_app = App( import_paths=[ "app.tasks.adventure", "app.tasks.regenerate_audio", - "app.tasks.summarise", + "app.tasks.create_summary_article", ], ) diff --git a/api/app/tasks/summarise.py b/api/app/tasks/create_summary_article.py similarity index 64% rename from api/app/tasks/summarise.py rename to api/app/tasks/create_summary_article.py index 50f2ca3..7323ca9 100644 --- a/api/app/tasks/summarise.py +++ b/api/app/tasks/create_summary_article.py @@ -1,6 +1,12 @@ import logging import uuid +from sqlalchemy.ext.asyncio import AsyncSession + +from app.outbound.postgres.repositories.article_repository import ( + PostgresArticleRepository, +) + from ..config import settings from ..domain.services.summarise_service import SummariseService from ..outbound.anthropic.anthropic_client import AnthropicClient @@ -14,32 +20,27 @@ from .app import procrastinate_app logger = logging.getLogger(__name__) -def _make_summarise_service() -> SummariseService: +def _make_summarise_service(db: AsyncSession) -> SummariseService: return SummariseService( anthropic_client=AnthropicClient.new(settings.anthropic_api_key), deepgram_client=LocalDeepgramClient(settings.deepgram_api_key), deepl_client=DeepLClient(settings.deepl_api_key), gemini_client=GeminiClient(settings.gemini_api_key), spacy_client=SpacyClient(), + article_repository=PostgresArticleRepository(db), ) @procrastinate_app.task(queue="default") -async def summarise_article( - job_id: str, - article_id: str, - source_language: str, - target_language: str, - complexity_level: str, - input_texts: list[str], +async def create_summary_article( + article_id: str, target_language: str, complexity_level: str, input_text: str ) -> None: + print(f"Starting summarisation task for article_id={article_id}") async with AsyncSessionLocal() as db: - await _make_summarise_service().run( - db=db, - job_id=uuid.UUID(job_id), + print("Session opened, calling summarise service...") + await _make_summarise_service(db).summarise_article( article_id=uuid.UUID(article_id), - source_language=source_language, target_language=target_language, complexity_level=complexity_level, - input_texts=input_texts, + input_text=input_text, ) diff --git a/api/conftest.py b/api/conftest.py new file mode 100644 index 0000000..556fbed --- /dev/null +++ b/api/conftest.py @@ -0,0 +1,66 @@ +""" +Session-scoped fixtures that spin up and tear down the test stack. + +The test stack uses docker-compose.test.yml which: +- Runs on port 18000 (won't collide with the dev stack on 8000) +- Uses tmpfs for all storage (no data survives after `down`) +- Uses project name "langlearn-test" to stay isolated from dev containers +""" + +import pathlib +import subprocess +from dotenv import load_dotenv +import uuid + +import httpx +import pytest + +PROJECT_ROOT = pathlib.Path(__file__).parent.parent +COMPOSE_FILE = str(PROJECT_ROOT / "docker-compose.test.yml") +ENV_FILE = str(PROJECT_ROOT / ".env.test") +COMPOSE_PROJECT = "langlearn-test" +API_BASE_URL = "http://localhost:18000" + +load_dotenv(PROJECT_ROOT / ".env.test") + +def _compose(*args: str) -> None: + subprocess.run( + ["docker", "compose", "-p", COMPOSE_PROJECT, "-f", COMPOSE_FILE, *args], + cwd=PROJECT_ROOT, + check=True, + ) + + +@pytest.fixture(scope="session", autouse=True) +def docker_stack(): + """Bring the test stack up before the session; tear it down (including volumes) after.""" + _compose("--env-file", ENV_FILE, "up", "--build", "--wait", "-d") + yield + _compose("down", "-v") + + +@pytest.fixture +def client() -> httpx.Client: + """A plain httpx client pointed at the test API. Not authenticated.""" + with httpx.Client(base_url=API_BASE_URL) as c: + yield c + +def _random_email() -> str: + return f"user-{uuid.uuid4()}@example.com" + +@pytest.fixture +def authd_client() -> httpx.Client: + email = _random_email() + password = "password1234" + + with httpx.Client(base_url=API_BASE_URL) as client: + register_response = client.post("/api/auth/register", json={"email": email, "password": password}) + assert register_response.json().get("success") is True, f"Failed to register test user: {register_response.text}" + + login_response = client.post("/api/auth/login", json={"email": email, "password": password}) + assert login_response.status_code == 200, f"Failed to log in test user: {login_response.text}" + + token = login_response.json().get("access_token") + client.headers["Authorization"] = f"Bearer {token}" + yield client + diff --git a/api/pyproject.toml b/api/pyproject.toml index c5ecabd..143b3c0 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "prometheus-fastapi-instrumentator>=7.1.0", "procrastinate>=3.8.1", "watchfiles>=1.0.0", + "python-dotenv>=1.2.2", ] [build-system] @@ -42,3 +43,9 @@ dev = [ "pytest>=9.0.3", "pytest-asyncio>=1.3.0", ] + +[tool.pytest.ini_options] +testpaths = ["."] + +[pytest] +addopts = ["--import-mode=importlib"] diff --git a/api/tests/__init__.py b/api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/tests/test_article_router.py b/api/tests/test_article_router.py new file mode 100644 index 0000000..534cef1 --- /dev/null +++ b/api/tests/test_article_router.py @@ -0,0 +1,20 @@ +import httpx +from fastapi.testclient import TestClient + +from app.main import app + + + +def test_create_article(authd_client: httpx.Client): + create_summary_article_response = authd_client.post("/api/articles", json={ + "article_type": "summary", + "target_language": "fr", + "competency_level": "B2", + "target_word_count_range": "250-300", + "input_text": "This is an example of a very long text" + }) + assert create_summary_article_response.status_code == 201 + + article_id = create_summary_article_response.json().get("id") + get_response = authd_client.get(f"/api/articles/{article_id}") + assert get_response.status_code == 200 diff --git a/api/uv.lock b/api/uv.lock index 8488733..44ac871 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -417,7 +417,7 @@ requests = [ [[package]] name = "google-genai" -version = "1.70.0" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -431,9 +431,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/dd/28e4682904b183acbfad3fe6409f13a42f69bb8eab6e882d3bcbea1dde01/google_genai-1.70.0.tar.gz", hash = "sha256:36b67b0fc6f319e08d1f1efd808b790107b1809c8743a05d55dfcf9d9fad7719", size = 519550, upload-time = "2026-04-01T10:52:46.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/7b/6eb3b3d545b6bb4c374acba1ccf91b0f33b605e551536a6243cfcef2f07f/google_genai-2.7.0.tar.gz", hash = "sha256:3c6f32f5ced9877ededd1b384b5e5b7f09c20046ec3390b662b16d8cd1882ac5", size = 555853, upload-time = "2026-05-28T15:39:24.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/a3/d4564c8a9beaf6a3cef8d70fa6354318572cebfee65db4f01af0d41f45ba/google_genai-1.70.0-py3-none-any.whl", hash = "sha256:b74c24549d8b4208f4c736fd11857374788e1ffffc725de45d706e35c97fceee", size = 760584, upload-time = "2026-04-01T10:52:44.349Z" }, + { url = "https://files.pythonhosted.org/packages/3c/dd/7a8be39e9d698e80e9db796514efbc6083dbd787bdb9a101e8ba47248e5e/google_genai-2.7.0-py3-none-any.whl", hash = "sha256:21cac381e09a869151706aba797b6a4f96cfe92c484e13204d092caee7ff11cb", size = 822545, upload-time = "2026-05-28T15:39:22.907Z" }, ] [[package]] @@ -595,6 +595,7 @@ dependencies = [ { name = "prometheus-fastapi-instrumentator" }, { name = "pydantic-settings" }, { name = "pyjwt" }, + { name = "python-dotenv" }, { name = "spacy" }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "uvicorn", extra = ["standard"] }, @@ -617,7 +618,7 @@ requires-dist = [ { name = "deepgram-sdk", specifier = ">=6.1.0" }, { name = "email-validator", specifier = ">=2.0.0" }, { name = "fastapi", specifier = ">=0.115.0" }, - { name = "google-genai", specifier = ">=1.0.0" }, + { name = "google-genai", specifier = ">=2.6.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "opentelemetry-api", specifier = ">=1.42.1" }, { name = "opentelemetry-exporter-prometheus", specifier = ">=0.63b1" }, @@ -630,6 +631,7 @@ requires-dist = [ { name = "prometheus-fastapi-instrumentator", specifier = ">=7.1.0" }, { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pyjwt", specifier = ">=2.10.0" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "spacy", specifier = ">=3.8.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" }, diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index f14d1b0..2ed011d 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -103,6 +103,7 @@ services: context: ./frontend args: PUBLIC_API_BASE_URL: ${PUBLIC_API_BASE_URL:-http://api:8000} + command: sh -c "npm install && npm run dev" ports: - "${FRONTEND_PORT:-3001}:3001" environment: diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index 235d5ee..154c29d 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -67,6 +67,8 @@ services: worker: build: ./api + volumes: + - ./api:/app:z command: python -m worker.main environment: DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-langlearn}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-langlearn} diff --git a/docker-compose.test.yml b/docker-compose.test.yml index d27ecc9..29376d1 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -37,6 +37,7 @@ services: - "18000:8000" environment: DATABASE_URL: postgresql+asyncpg://langlearn_test:testpassword@db:5432/langlearn_test + PROCRASTINATE_DATABASE_URL: postgresql://${POSTGRES_USER:-langlearn}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-langlearn} JWT_SECRET: test-jwt-secret-not-for-production ANTHROPIC_API_KEY: test-key DEEPL_API_KEY: test-key @@ -49,6 +50,7 @@ services: STORAGE_SECRET_KEY: testpassword123 STORAGE_BUCKET: langlearn-test STUB_GENERATION: "true" + TRANSACTIONAL_EMAIL_PROVIDER: stub depends_on: db: condition: service_healthy @@ -65,9 +67,12 @@ services: worker: build: ./api + volumes: + - ./api:/app:z command: python -m worker.main environment: DATABASE_URL: postgresql+asyncpg://langlearn_test:testpassword@db:5432/langlearn_test + PROCRASTINATE_DATABASE_URL: postgresql://langlearn_test:testpassword@db:5432/langlearn_test JWT_SECRET: test-jwt-secret-not-for-production ANTHROPIC_API_KEY: test-key DEEPL_API_KEY: test-key diff --git a/frontend/docs/openapi.json b/frontend/docs/openapi.json index 812f611..796383e 100644 --- a/frontend/docs/openapi.json +++ b/frontend/docs/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Language Learning API","version":"0.1.0"},"paths":{"/api/account/learnable-languages":{"post":{"tags":["api","account"],"summary":"Add Learnable Language","operationId":"add_learnable_language_api_account_learnable_languages_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddLearnableLanguageRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearnableLanguageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/account/onboarding":{"post":{"tags":["api","account"],"summary":"Complete Onboarding","operationId":"complete_onboarding_api_account_onboarding_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Complete Onboarding Api Account Onboarding Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/account/status":{"get":{"tags":["api","account"],"summary":"Get Account Status","operationId":"get_account_status_api_account_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountStatusResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/account/learnable-languages/{language_id}":{"delete":{"tags":["api","account"],"summary":"Remove Learnable Language","operationId":"remove_learnable_language_api_account_learnable_languages__language_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"language_id","in":"path","required":true,"schema":{"type":"string","title":"Language Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs":{"post":{"tags":["api","admin-packs"],"summary":"Create Pack","operationId":"create_pack_api_admin_packs_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePackRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["api","admin-packs"],"summary":"List Packs","operationId":"list_packs_api_admin_packs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"source_lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Lang"}},{"name":"target_lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PackResponse"},"title":"Response List Packs Api Admin Packs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}":{"get":{"tags":["api","admin-packs"],"summary":"Get Pack","operationId":"get_pack_api_admin_packs__pack_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__routers__api__admin__packs__PackDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["api","admin-packs"],"summary":"Update Pack","operationId":"update_pack_api_admin_packs__pack_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePackRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/publish":{"post":{"tags":["api","admin-packs"],"summary":"Publish Pack","operationId":"publish_pack_api_admin_packs__pack_id__publish_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/entries":{"post":{"tags":["api","admin-packs"],"summary":"Add Entry","operationId":"add_entry_api_admin_packs__pack_id__entries_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddEntryRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackEntryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/entries/{entry_id}":{"delete":{"tags":["api","admin-packs"],"summary":"Remove Entry","operationId":"remove_entry_api_admin_packs__pack_id__entries__entry_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}},{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/entries/{entry_id}/flashcards":{"post":{"tags":["api","admin-packs"],"summary":"Add Flashcard Template","operationId":"add_flashcard_template_api_admin_packs__pack_id__entries__entry_id__flashcards_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}},{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddFlashcardTemplateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlashcardTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/entries/{entry_id}/flashcards/{template_id}":{"delete":{"tags":["api","admin-packs"],"summary":"Remove Flashcard Template","operationId":"remove_flashcard_template_api_admin_packs__pack_id__entries__entry_id__flashcards__template_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}},{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}},{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/adventures":{"get":{"tags":["api","adventures"],"summary":"List Adventures","operationId":"list_adventures_api_adventures_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AdventureResponse"},"type":"array","title":"Response List Adventures Api Adventures Get"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["api","adventures"],"summary":"Create Adventure","operationId":"create_adventure_api_adventures_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdventureRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdventureResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/adventures/{adventure_id}":{"get":{"tags":["api","adventures"],"summary":"Get Adventure","operationId":"get_adventure_api_adventures__adventure_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdventureResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["api","adventures"],"summary":"Delete Adventure","operationId":"delete_adventure_api_adventures__adventure_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/adventures/{adventure_id}/decisions":{"post":{"tags":["api","adventures"],"summary":"Record Decision","operationId":"record_decision_api_adventures__adventure_id__decisions_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDecisionRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/adventures/{adventure_id}/entries":{"get":{"tags":["api","adventures"],"summary":"List Entries","operationId":"list_entries_api_adventures__adventure_id__entries_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EntryResponse"},"title":"Response List Entries Api Adventures Adventure Id Entries Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/adventures/{adventure_id}/entries/{entry_id}":{"get":{"tags":["api","adventures"],"summary":"Get Entry","operationId":"get_entry_api_adventures__adventure_id__entries__entry_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}},{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntryDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/articles":{"post":{"tags":["api","adventures"],"summary":"Create Article","operationId":"create_article_api_articles_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/articles/{article_id}":{"get":{"tags":["api","adventures"],"summary":"Get Article","operationId":"get_article_api_articles__article_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__routers__api__articles__ArticleItem"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/register":{"post":{"tags":["api","auth"],"summary":"Register","operationId":"register_api_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/login":{"post":{"tags":["api","auth"],"summary":"Login","operationId":"login_api_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/verify-email":{"get":{"tags":["api","auth"],"summary":"Verify Email","operationId":"verify_email_api_auth_verify_email_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Verify Email Api Auth Verify Email Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dictionary/search":{"get":{"tags":["api","dictionary"],"summary":"Search Wordforms Prefix","description":"Search for wordforms whose surface text starts with the given prefix.\n\nUses accent-insensitive, case-insensitive prefix matching so that e.g.\n\"chatea\" returns both \"château\" and \"châteaux\", and \"lent\" returns all\nfour forms of the adjective. Returns one entry per matching lemma.","operationId":"search_wordforms_prefix_api_dictionary_search_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"lang_code","in":"query","required":true,"schema":{"type":"string","title":"Lang Code"}},{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WordformMatch"},"title":"Response Search Wordforms Prefix Api Dictionary Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dictionary/senses":{"get":{"tags":["api","dictionary"],"summary":"Search Senses","description":"Search for a Sense by (English) definition\n\nReturns one entry per matching senses,each with its Sense.","operationId":"search_senses_api_dictionary_senses_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"lang_code","in":"query","required":true,"schema":{"type":"string","title":"Lang Code"}},{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SenseMatch"},"title":"Response Search Senses Api Dictionary Senses Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dictionary/wordforms":{"get":{"tags":["api","dictionary"],"summary":"Search Wordforms","description":"Search for a wordform by surface text within a language.\n\nReturns one entry per matching lemma, each with the lemma's senses. A single\nform (e.g. \"allons\") may resolve to more than one lemma when homographs exist.","operationId":"search_wordforms_api_dictionary_wordforms_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"lang_code","in":"query","required":true,"schema":{"type":"string","title":"Lang Code"}},{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WordformMatch"},"title":"Response Search Wordforms Api Dictionary Wordforms Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/vocab/{entry_id}/flashcards":{"post":{"tags":["api","flashcards"],"summary":"Generate Flashcards","operationId":"generate_flashcards_api_vocab__entry_id__flashcards_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateFlashcardsRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FlashcardResponse"},"title":"Response Generate Flashcards Api Vocab Entry Id Flashcards Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/flashcards":{"get":{"tags":["api","flashcards"],"summary":"List Flashcards","operationId":"list_flashcards_api_flashcards_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/FlashcardResponse"},"type":"array","title":"Response List Flashcards Api Flashcards Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/flashcards/{flashcard_id}/events":{"post":{"tags":["api","flashcards"],"summary":"Record Event","operationId":"record_event_api_flashcards__flashcard_id__events_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flashcard_id","in":"path","required":true,"schema":{"type":"string","title":"Flashcard Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordEventRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlashcardEventResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/generate":{"post":{"tags":["api","api"],"summary":"Create Generation Job","operationId":"create_generation_job_api_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/jobs/":{"get":{"tags":["api"],"summary":"Get Jobs","operationId":"get_jobs_api_jobs__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobListResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/jobs/{job_id}":{"get":{"tags":["api"],"summary":"Get Job","operationId":"get_job_api_jobs__job_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/learnable_languages":{"post":{"tags":["api","api"],"summary":"Upsert Learnable Language","operationId":"upsert_learnable_language_api_learnable_languages_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearnableLanguageRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearnableLanguageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/packs":{"get":{"tags":["api","packs"],"summary":"List Packs","operationId":"list_packs_api_packs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"source_lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Lang"}},{"name":"target_lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PackSummaryResponse"},"title":"Response List Packs Api Packs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/packs/{pack_id}":{"get":{"tags":["api","packs"],"summary":"Get Pack","operationId":"get_pack_api_packs__pack_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__routers__api__packs__PackDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/packs/{pack_id}/add-to-bank":{"post":{"tags":["api","packs"],"summary":"Add Pack To Bank","operationId":"add_pack_to_bank_api_packs__pack_id__add_to_bank_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddTobankRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddTobankResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/pos/":{"post":{"tags":["api","api","pos"],"summary":"Analyze Pos","operationId":"analyze_pos_api_pos__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/POSRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/POSResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/translate":{"get":{"tags":["api","api","translate"],"summary":"Translate text to a target language","operationId":"translate_text_api_translate_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"target_language","in":"query","required":true,"schema":{"type":"string","title":"Target Language"}},{"name":"context","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Context"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/vocab":{"post":{"tags":["api","vocab"],"summary":"Add Word","operationId":"add_word_api_vocab_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddWordRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WordBankEntryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["api","vocab"],"summary":"List Entries","operationId":"list_entries_api_vocab_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"language_pair_id","in":"query","required":true,"schema":{"type":"string","title":"Language Pair Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WordBankEntryResponse"},"title":"Response List Entries Api Vocab Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/vocab/from-token":{"post":{"tags":["api","vocab"],"summary":"Add From Token","operationId":"add_from_token_api_vocab_from_token_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddFromTokenRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FromTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/vocab/pending-disambiguation":{"get":{"tags":["api","vocab"],"summary":"Pending Disambiguation","operationId":"pending_disambiguation_api_vocab_pending_disambiguation_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/WordBankEntryResponse"},"type":"array","title":"Response Pending Disambiguation Api Vocab Pending Disambiguation Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/vocab/{entry_id}/sense":{"patch":{"tags":["api","vocab"],"summary":"Resolve Sense","operationId":"resolve_sense_api_vocab__entry_id__sense_patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetSenseRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WordBankEntryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bff/account":{"get":{"tags":["bff","bff"],"summary":"Get Account","operationId":"get_account_bff_account_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/bff/account/onboarding":{"get":{"tags":["bff","bff"],"summary":"Get Onboarding","operationId":"get_onboarding_bff_account_onboarding_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/bff/adventure/{adventure_id}":{"get":{"tags":["bff","bff","adventures"],"summary":"Get Adventure","operationId":"get_adventure_bff_adventure__adventure_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdventureDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bff/articles":{"get":{"tags":["bff","bff","articles"],"summary":"List Articles","operationId":"list_articles_bff_articles_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"target_language","in":"query","required":false,"schema":{"type":"string","default":"fr","title":"Target Language"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bff/articles/{article_id}":{"get":{"tags":["bff","bff","articles"],"summary":"Get Article","operationId":"get_article_bff_articles__article_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bff/user_profile":{"get":{"tags":["bff","bff"],"summary":"Get User Profile","operationId":"get_user_profile_bff_user_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfileResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/bff/packs":{"get":{"tags":["bff","bff-packs"],"summary":"List Packs For Selection","operationId":"list_packs_for_selection_bff_packs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"source_lang","in":"query","required":true,"schema":{"type":"string","title":"Source Lang"}},{"name":"target_lang","in":"query","required":true,"schema":{"type":"string","title":"Target Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PackSelectionItem"},"title":"Response List Packs For Selection Bff Packs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/media/adventure-audio/{filename}":{"get":{"tags":["media"],"summary":"Get Adventure Audio File","operationId":"get_adventure_audio_file_media_adventure_audio__filename__get","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/media/{filename}":{"get":{"tags":["media"],"summary":"Get Media File","operationId":"get_media_file_media__filename__get","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/metrics":{"get":{"summary":"Metrics","description":"Endpoint that serves Prometheus metrics.","operationId":"metrics_metrics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Health Health Get"}}}}}}}},"components":{"schemas":{"AccountLanguagePair":{"properties":{"id":{"type":"string","title":"Id"},"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["id","source_language","target_language","proficiencies"],"title":"AccountLanguagePair"},"AccountResponse":{"properties":{"email":{"type":"string","title":"Email"},"human_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Human Name"},"language_pairs":{"items":{"$ref":"#/components/schemas/AccountLanguagePair"},"type":"array","title":"Language Pairs"}},"type":"object","required":["email","human_name","language_pairs"],"title":"AccountResponse"},"AccountStatusResponse":{"properties":{"problem_flags":{"items":{"type":"string"},"type":"array","title":"Problem Flags"},"error_messages":{"items":{"type":"string"},"type":"array","title":"Error Messages"}},"type":"object","required":["problem_flags","error_messages"],"title":"AccountStatusResponse"},"AddEntryRequest":{"properties":{"sense_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sense Id"},"surface_text":{"type":"string","title":"Surface Text"}},"type":"object","required":["surface_text"],"title":"AddEntryRequest"},"AddFlashcardTemplateRequest":{"properties":{"prompt_text":{"type":"string","title":"Prompt Text"},"answer_text":{"type":"string","title":"Answer Text"},"prompt_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt Context Text"},"answer_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Context Text"}},"type":"object","required":["prompt_text","answer_text"],"title":"AddFlashcardTemplateRequest"},"AddFromTokenRequest":{"properties":{"language_pair_id":{"type":"string","title":"Language Pair Id"},"surface":{"type":"string","title":"Surface"},"spacy_lemma":{"type":"string","title":"Spacy Lemma"},"pos_ud":{"type":"string","title":"Pos Ud"},"language":{"type":"string","title":"Language"},"source_article_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Article Id"}},"type":"object","required":["language_pair_id","surface","spacy_lemma","pos_ud","language"],"title":"AddFromTokenRequest"},"AddLearnableLanguageRequest":{"properties":{"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["source_language","target_language","proficiencies"],"title":"AddLearnableLanguageRequest"},"AddTobankRequest":{"properties":{"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"}},"type":"object","required":["source_lang","target_lang"],"title":"AddTobankRequest"},"AddTobankResponse":{"properties":{"added":{"items":{"type":"string"},"type":"array","title":"Added"}},"type":"object","required":["added"],"title":"AddTobankResponse"},"AddWordRequest":{"properties":{"language_pair_id":{"type":"string","title":"Language Pair Id"},"surface_text":{"type":"string","title":"Surface Text"},"entry_pathway":{"type":"string","title":"Entry Pathway","default":"manual"},"is_phrase":{"type":"boolean","title":"Is Phrase","default":false},"source_article_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Article Id"}},"type":"object","required":["language_pair_id","surface_text"],"title":"AddWordRequest"},"AdventureChoiceItem":{"properties":{"id":{"type":"string","title":"Id"},"index":{"type":"integer","title":"Index"},"label":{"type":"string","title":"Label"},"text":{"type":"string","title":"Text"}},"type":"object","required":["id","index","label","text"],"title":"AdventureChoiceItem"},"AdventureDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"status":{"type":"string","title":"Status"},"language":{"type":"string","title":"Language"},"source_language":{"type":"string","title":"Source Language"},"competencies":{"items":{"type":"string"},"type":"array","title":"Competencies"},"max_entry_count":{"type":"integer","title":"Max Entry Count"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"setting":{"items":{"type":"string"},"type":"array","title":"Setting"},"vibes":{"items":{"type":"string"},"type":"array","title":"Vibes"},"protagonist":{"items":{"type":"string"},"type":"array","title":"Protagonist"},"created_at":{"type":"string","title":"Created At"},"entries":{"items":{"$ref":"#/components/schemas/AdventureEntryItem"},"type":"array","title":"Entries"},"current_entry_choices":{"items":{"$ref":"#/components/schemas/AdventureChoiceItem"},"type":"array","title":"Current Entry Choices"}},"type":"object","required":["id","user_id","status","language","source_language","competencies","max_entry_count","title","description","genres","setting","vibes","protagonist","created_at","entries","current_entry_choices"],"title":"AdventureDetailResponse"},"AdventureEntryItem":{"properties":{"id":{"type":"string","title":"Id"},"adventure_id":{"type":"string","title":"Adventure Id"},"possible_choices":{"anyOf":[{"items":{"$ref":"#/components/schemas/AdventureChoiceItem"},"type":"array"},{"type":"null"}],"title":"Possible Choices"},"generated_from_choice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated From Choice Id"},"status":{"type":"string","title":"Status"},"entry_index":{"type":"integer","title":"Entry Index"},"story_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Story Text"},"story_text_linguistic_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Story Text Linguistic Data"},"translation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Translation"},"audio_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio Url"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","adventure_id","possible_choices","generated_from_choice_id","status","entry_index","story_text","story_text_linguistic_data","translation","audio_url","created_at"],"title":"AdventureEntryItem"},"AdventureResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"status":{"type":"string","title":"Status"},"language":{"type":"string","title":"Language"},"source_language":{"type":"string","title":"Source Language"},"competencies":{"items":{"type":"string"},"type":"array","title":"Competencies"},"max_entry_count":{"type":"integer","title":"Max Entry Count"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"setting":{"items":{"type":"string"},"type":"array","title":"Setting"},"vibes":{"items":{"type":"string"},"type":"array","title":"Vibes"},"protagonist":{"items":{"type":"string"},"type":"array","title":"Protagonist"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","user_id","status","language","source_language","competencies","max_entry_count","title","description","genres","setting","vibes","protagonist","created_at"],"title":"AdventureResponse"},"ArticleDetail":{"properties":{"id":{"type":"string","title":"Id"},"published_at":{"type":"string","format":"date-time","title":"Published At"},"source_language":{"type":"string","title":"Source Language"},"source_title":{"type":"string","title":"Source Title"},"source_body":{"type":"string","title":"Source Body"},"source_body_pos":{"additionalProperties":true,"type":"object","title":"Source Body Pos"},"target_language":{"type":"string","title":"Target Language"},"target_complexities":{"items":{"type":"string"},"type":"array","title":"Target Complexities"},"target_title":{"type":"string","title":"Target Title"},"target_body":{"type":"string","title":"Target Body"},"target_audio_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Audio Url"},"target_body_pos":{"additionalProperties":true,"type":"object","title":"Target Body Pos"},"target_body_transcript":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Target Body Transcript"}},"type":"object","required":["id","published_at","source_language","source_title","source_body","source_body_pos","target_language","target_complexities","target_title","target_body","target_audio_url","target_body_pos","target_body_transcript"],"title":"ArticleDetail"},"ArticleListResponse":{"properties":{"articles":{"items":{"$ref":"#/components/schemas/app__routers__bff__articles__ArticleItem"},"type":"array","title":"Articles"}},"type":"object","required":["articles"],"title":"ArticleListResponse"},"ArticleTypeEnum":{"type":"string","enum":["summary"],"title":"ArticleTypeEnum"},"ChoiceResponse":{"properties":{"id":{"type":"string","title":"Id"},"index":{"type":"integer","title":"Index"},"label":{"type":"string","title":"Label"},"text":{"type":"string","title":"Text"}},"type":"object","required":["id","index","label","text"],"title":"ChoiceResponse"},"CreateAdventureRequest":{"properties":{"language":{"type":"string","title":"Language"},"source_language":{"type":"string","title":"Source Language"},"competencies":{"items":{"type":"string"},"type":"array","title":"Competencies"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"setting":{"items":{"type":"string"},"type":"array","title":"Setting"},"vibes":{"items":{"type":"string"},"type":"array","title":"Vibes"},"protagonist":{"items":{"type":"string"},"type":"array","title":"Protagonist"},"entry_word_count_range":{"type":"string","title":"Entry Word Count Range"},"max_entry_count":{"type":"integer","title":"Max Entry Count","default":6}},"type":"object","required":["language","source_language","competencies","genres","setting","vibes","protagonist","entry_word_count_range"],"title":"CreateAdventureRequest"},"CreateArticleBody":{"properties":{"article_type":{"$ref":"#/components/schemas/ArticleTypeEnum"},"language":{"type":"string","title":"Language"},"target_complexity":{"type":"string","title":"Target Complexity"},"text":{"type":"string","title":"Text"}},"type":"object","required":["article_type","language","target_complexity","text"],"title":"CreateArticleBody"},"CreateArticleResponse":{"properties":{"id":{"type":"string","title":"Id"}},"type":"object","required":["id"],"title":"CreateArticleResponse"},"CreateDecisionRequest":{"properties":{"choice_id":{"type":"string","title":"Choice Id"}},"type":"object","required":["choice_id"],"title":"CreateDecisionRequest"},"CreatePackRequest":{"properties":{"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies","default":[]}},"type":"object","required":["name","name_target","description","description_target","source_lang","target_lang"],"title":"CreatePackRequest"},"DecisionResponse":{"properties":{"id":{"type":"string","title":"Id"},"choice_id":{"type":"string","title":"Choice Id"},"user_id":{"type":"string","title":"User Id"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","choice_id","user_id","created_at"],"title":"DecisionResponse"},"EntryDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"adventure_id":{"type":"string","title":"Adventure Id"},"generated_from_choice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated From Choice Id"},"status":{"type":"string","title":"Status"},"entry_index":{"type":"integer","title":"Entry Index"},"story_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Story Text"},"created_at":{"type":"string","title":"Created At"},"choices":{"items":{"$ref":"#/components/schemas/ChoiceResponse"},"type":"array","title":"Choices"},"translation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Translation"},"audio_file_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio File Name"},"story_text_linguistic_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Story Text Linguistic Data"}},"type":"object","required":["id","adventure_id","generated_from_choice_id","status","entry_index","story_text","created_at","choices","translation","audio_file_name","story_text_linguistic_data"],"title":"EntryDetailResponse"},"EntryResponse":{"properties":{"id":{"type":"string","title":"Id"},"adventure_id":{"type":"string","title":"Adventure Id"},"generated_from_choice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated From Choice Id"},"status":{"type":"string","title":"Status"},"entry_index":{"type":"integer","title":"Entry Index"},"story_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Story Text"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","adventure_id","generated_from_choice_id","status","entry_index","story_text","created_at"],"title":"EntryResponse"},"FlashcardEventResponse":{"properties":{"id":{"type":"string","title":"Id"},"flashcard_id":{"type":"string","title":"Flashcard Id"},"user_id":{"type":"string","title":"User Id"},"event_type":{"type":"string","title":"Event Type"},"user_response":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Response"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","flashcard_id","user_id","event_type","user_response","created_at"],"title":"FlashcardEventResponse"},"FlashcardResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"bank_entry_id":{"type":"string","title":"Bank Entry Id"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"prompt_text":{"type":"string","title":"Prompt Text"},"answer_text":{"type":"string","title":"Answer Text"},"prompt_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt Context Text"},"answer_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Context Text"},"prompt_modality":{"type":"string","title":"Prompt Modality"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","user_id","bank_entry_id","source_lang","target_lang","prompt_text","answer_text","prompt_context_text","answer_context_text","prompt_modality","created_at"],"title":"FlashcardResponse"},"FlashcardTemplateResponse":{"properties":{"id":{"type":"string","title":"Id"},"pack_entry_id":{"type":"string","title":"Pack Entry Id"},"prompt_text":{"type":"string","title":"Prompt Text"},"answer_text":{"type":"string","title":"Answer Text"},"prompt_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt Context Text"},"answer_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Context Text"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","pack_entry_id","prompt_text","answer_text","prompt_context_text","answer_context_text","created_at"],"title":"FlashcardTemplateResponse"},"FromTokenResponse":{"properties":{"entry":{"$ref":"#/components/schemas/WordBankEntryResponse"},"sense_candidates":{"items":{"$ref":"#/components/schemas/SenseCandidateResponse"},"type":"array","title":"Sense Candidates"},"matched_via":{"type":"string","title":"Matched Via"}},"type":"object","required":["entry","sense_candidates","matched_via"],"title":"FromTokenResponse"},"GenerateFlashcardsRequest":{"properties":{"direction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Direction"}},"type":"object","title":"GenerateFlashcardsRequest"},"GenerationRequest":{"properties":{"target_language":{"type":"string","title":"Target Language"},"complexity_level":{"type":"string","title":"Complexity Level"},"input_texts":{"items":{"type":"string"},"type":"array","title":"Input Texts"},"source_language":{"type":"string","title":"Source Language","default":"en"}},"type":"object","required":["target_language","complexity_level","input_texts"],"title":"GenerationRequest"},"GenerationResponse":{"properties":{"article_id":{"type":"string","title":"Article Id"}},"type":"object","required":["article_id"],"title":"GenerationResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"JobListResponse":{"properties":{"jobs":{"items":{"$ref":"#/components/schemas/JobSummary"},"type":"array","title":"Jobs"}},"type":"object","required":["jobs"],"title":"JobListResponse"},"JobResponse":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"status":{"type":"string","title":"Status"},"translated_article_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Translated Article Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Started At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"}},"type":"object","required":["id","status","created_at"],"title":"JobResponse"},"JobSummary":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"}},"type":"object","required":["id","status","created_at"],"title":"JobSummary"},"LanguagePairOption":{"properties":{"value":{"type":"string","title":"Value"},"label":{"type":"string","title":"Label"},"description":{"type":"string","title":"Description"}},"type":"object","required":["value","label","description"],"title":"LanguagePairOption"},"LearnableLanguageItem":{"properties":{"id":{"type":"string","title":"Id"},"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["id","source_language","target_language","proficiencies"],"title":"LearnableLanguageItem"},"LearnableLanguageRequest":{"properties":{"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["source_language","target_language","proficiencies"],"title":"LearnableLanguageRequest"},"LearnableLanguageResponse":{"properties":{"id":{"type":"string","title":"Id"},"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["id","source_language","target_language","proficiencies"],"title":"LearnableLanguageResponse"},"LemmaResponse":{"properties":{"id":{"type":"string","title":"Id"},"headword":{"type":"string","title":"Headword"},"language":{"type":"string","title":"Language"},"pos_raw":{"type":"string","title":"Pos Raw"},"pos_normalised":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pos Normalised"},"gender":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gender"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"}},"type":"object","required":["id","headword","language","pos_raw","pos_normalised","gender","tags"],"title":"LemmaResponse"},"LoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"LoginRequest"},"OnboardingRequest":{"properties":{"human_name":{"type":"string","title":"Human Name"},"language_pairs":{"items":{"type":"string"},"type":"array","title":"Language Pairs"},"proficiencies":{"items":{"items":{"type":"string"},"type":"array"},"type":"array","title":"Proficiencies"}},"type":"object","required":["human_name","language_pairs","proficiencies"],"title":"OnboardingRequest"},"OnboardingResponse":{"properties":{"language_pairs":{"items":{"$ref":"#/components/schemas/LanguagePairOption"},"type":"array","title":"Language Pairs"},"proficiencies":{"items":{"$ref":"#/components/schemas/ProficiencyOption"},"type":"array","title":"Proficiencies"}},"type":"object","required":["language_pairs","proficiencies"],"title":"OnboardingResponse"},"POSRequest":{"properties":{"text":{"type":"string","title":"Text"},"language":{"type":"string","title":"Language"}},"type":"object","required":["text","language"],"title":"POSRequest"},"POSResponse":{"properties":{"language":{"type":"string","title":"Language"},"tokens":{"items":{"$ref":"#/components/schemas/TokenInfo"},"type":"array","title":"Tokens"}},"type":"object","required":["language","tokens"],"title":"POSResponse"},"PackEntryResponse":{"properties":{"id":{"type":"string","title":"Id"},"pack_id":{"type":"string","title":"Pack Id"},"sense_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sense Id"},"surface_text":{"type":"string","title":"Surface Text"},"created_at":{"type":"string","title":"Created At"},"flashcard_templates":{"items":{"$ref":"#/components/schemas/FlashcardTemplateResponse"},"type":"array","title":"Flashcard Templates","default":[]}},"type":"object","required":["id","pack_id","sense_id","surface_text","created_at"],"title":"PackEntryResponse"},"PackResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"is_published":{"type":"boolean","title":"Is Published"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","is_published","created_at"],"title":"PackResponse"},"PackSelectionItem":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"entry_count":{"type":"integer","title":"Entry Count"},"already_added":{"type":"boolean","title":"Already Added"}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","entry_count","already_added"],"title":"PackSelectionItem"},"PackSummaryResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"entry_count":{"type":"integer","title":"Entry Count"}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","entry_count"],"title":"PackSummaryResponse"},"ProficiencyOption":{"properties":{"value":{"type":"string","title":"Value"},"label":{"type":"string","title":"Label"},"description":{"type":"string","title":"Description"}},"type":"object","required":["value","label","description"],"title":"ProficiencyOption"},"RecordEventRequest":{"properties":{"event_type":{"type":"string","title":"Event Type"},"user_response":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Response"}},"type":"object","required":["event_type"],"title":"RecordEventRequest"},"RegisterRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"RegisterRequest"},"RegisterResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"}},"type":"object","required":["success"],"title":"RegisterResponse"},"SenseCandidateResponse":{"properties":{"id":{"type":"string","title":"Id"},"gloss":{"type":"string","title":"Gloss"},"topics":{"items":{"type":"string"},"type":"array","title":"Topics"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"}},"type":"object","required":["id","gloss","topics","tags"],"title":"SenseCandidateResponse"},"SenseMatch":{"properties":{"sense":{"$ref":"#/components/schemas/SenseResponse"},"lemma":{"$ref":"#/components/schemas/LemmaResponse"}},"type":"object","required":["sense","lemma"],"title":"SenseMatch"},"SenseResponse":{"properties":{"id":{"type":"string","title":"Id"},"sense_index":{"type":"integer","title":"Sense Index"},"gloss":{"type":"string","title":"Gloss"},"topics":{"items":{"type":"string"},"type":"array","title":"Topics"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"}},"type":"object","required":["id","sense_index","gloss","topics","tags"],"title":"SenseResponse"},"SetSenseRequest":{"properties":{"sense_id":{"type":"string","title":"Sense Id"}},"type":"object","required":["sense_id"],"title":"SetSenseRequest"},"TokenInfo":{"properties":{"text":{"type":"string","title":"Text"},"lemma":{"type":"string","title":"Lemma"},"pos":{"type":"string","title":"Pos"},"tag":{"type":"string","title":"Tag"},"dep":{"type":"string","title":"Dep"},"is_stop":{"type":"boolean","title":"Is Stop"}},"type":"object","required":["text","lemma","pos","tag","dep","is_stop"],"title":"TokenInfo"},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type","default":"bearer"}},"type":"object","required":["access_token"],"title":"TokenResponse"},"TranslationResponse":{"properties":{"text":{"type":"string","title":"Text"},"target_language":{"type":"string","title":"Target Language"},"translated_text":{"type":"string","title":"Translated Text"}},"type":"object","required":["text","target_language","translated_text"],"title":"TranslationResponse"},"UpdatePackRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"name_target":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name Target"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"description_target":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description Target"},"proficiencies":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Proficiencies"}},"type":"object","title":"UpdatePackRequest"},"UserProfileResponse":{"properties":{"learnable_languages":{"items":{"$ref":"#/components/schemas/LearnableLanguageItem"},"type":"array","title":"Learnable Languages"}},"type":"object","required":["learnable_languages"],"title":"UserProfileResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WordBankEntryResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"language_pair_id":{"type":"string","title":"Language Pair Id"},"sense_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sense Id"},"wordform_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wordform Id"},"surface_text":{"type":"string","title":"Surface Text"},"is_phrase":{"type":"boolean","title":"Is Phrase"},"entry_pathway":{"type":"string","title":"Entry Pathway"},"source_article_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Article Id"},"disambiguation_status":{"type":"string","title":"Disambiguation Status"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","user_id","language_pair_id","sense_id","wordform_id","surface_text","is_phrase","entry_pathway","source_article_id","disambiguation_status","created_at"],"title":"WordBankEntryResponse"},"WordformMatch":{"properties":{"lemma":{"$ref":"#/components/schemas/LemmaResponse"},"senses":{"items":{"$ref":"#/components/schemas/SenseResponse"},"type":"array","title":"Senses"}},"type":"object","required":["lemma","senses"],"title":"WordformMatch"},"app__routers__api__admin__packs__PackDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"is_published":{"type":"boolean","title":"Is Published"},"created_at":{"type":"string","title":"Created At"},"entries":{"items":{"$ref":"#/components/schemas/PackEntryResponse"},"type":"array","title":"Entries","default":[]}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","is_published","created_at"],"title":"PackDetailResponse"},"app__routers__api__articles__ArticleItem":{"properties":{"id":{"type":"string","title":"Id"}},"type":"object","required":["id"],"title":"ArticleItem"},"app__routers__api__packs__PackDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"entry_count":{"type":"integer","title":"Entry Count"},"surface_texts":{"items":{"type":"string"},"type":"array","title":"Surface Texts"}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","entry_count","surface_texts"],"title":"PackDetailResponse"},"app__routers__bff__articles__ArticleItem":{"properties":{"id":{"type":"string","title":"Id"},"published_at":{"type":"string","format":"date-time","title":"Published At"},"source_language":{"type":"string","title":"Source Language"},"source_title":{"type":"string","title":"Source Title"},"target_language":{"type":"string","title":"Target Language"},"target_complexities":{"items":{"type":"string"},"type":"array","title":"Target Complexities"},"target_title":{"type":"string","title":"Target Title"}},"type":"object","required":["id","published_at","source_language","source_title","target_language","target_complexities","target_title"],"title":"ArticleItem"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Language Learning API","version":"0.1.0"},"paths":{"/api/account/learnable-languages":{"post":{"tags":["api","account"],"summary":"Add Learnable Language","operationId":"add_learnable_language_api_account_learnable_languages_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddLearnableLanguageRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearnableLanguageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/account/onboarding":{"post":{"tags":["api","account"],"summary":"Complete Onboarding","operationId":"complete_onboarding_api_account_onboarding_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Complete Onboarding Api Account Onboarding Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/account/status":{"get":{"tags":["api","account"],"summary":"Get Account Status","operationId":"get_account_status_api_account_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountStatusResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/account/learnable-languages/{language_id}":{"delete":{"tags":["api","account"],"summary":"Remove Learnable Language","operationId":"remove_learnable_language_api_account_learnable_languages__language_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"language_id","in":"path","required":true,"schema":{"type":"string","title":"Language Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs":{"post":{"tags":["api","admin-packs"],"summary":"Create Pack","operationId":"create_pack_api_admin_packs_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePackRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["api","admin-packs"],"summary":"List Packs","operationId":"list_packs_api_admin_packs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"source_lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Lang"}},{"name":"target_lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PackResponse"},"title":"Response List Packs Api Admin Packs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}":{"get":{"tags":["api","admin-packs"],"summary":"Get Pack","operationId":"get_pack_api_admin_packs__pack_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__routers__api__admin__packs__PackDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["api","admin-packs"],"summary":"Update Pack","operationId":"update_pack_api_admin_packs__pack_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePackRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/publish":{"post":{"tags":["api","admin-packs"],"summary":"Publish Pack","operationId":"publish_pack_api_admin_packs__pack_id__publish_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/entries":{"post":{"tags":["api","admin-packs"],"summary":"Add Entry","operationId":"add_entry_api_admin_packs__pack_id__entries_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddEntryRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackEntryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/entries/{entry_id}":{"delete":{"tags":["api","admin-packs"],"summary":"Remove Entry","operationId":"remove_entry_api_admin_packs__pack_id__entries__entry_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}},{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/entries/{entry_id}/flashcards":{"post":{"tags":["api","admin-packs"],"summary":"Add Flashcard Template","operationId":"add_flashcard_template_api_admin_packs__pack_id__entries__entry_id__flashcards_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}},{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddFlashcardTemplateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlashcardTemplateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/packs/{pack_id}/entries/{entry_id}/flashcards/{template_id}":{"delete":{"tags":["api","admin-packs"],"summary":"Remove Flashcard Template","operationId":"remove_flashcard_template_api_admin_packs__pack_id__entries__entry_id__flashcards__template_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}},{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}},{"name":"template_id","in":"path","required":true,"schema":{"type":"string","title":"Template Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/adventures":{"get":{"tags":["api","adventures"],"summary":"List Adventures","operationId":"list_adventures_api_adventures_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AdventureResponse"},"type":"array","title":"Response List Adventures Api Adventures Get"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["api","adventures"],"summary":"Create Adventure","operationId":"create_adventure_api_adventures_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdventureRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdventureResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/adventures/{adventure_id}":{"get":{"tags":["api","adventures"],"summary":"Get Adventure","operationId":"get_adventure_api_adventures__adventure_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdventureResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["api","adventures"],"summary":"Delete Adventure","operationId":"delete_adventure_api_adventures__adventure_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/adventures/{adventure_id}/decisions":{"post":{"tags":["api","adventures"],"summary":"Record Decision","operationId":"record_decision_api_adventures__adventure_id__decisions_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDecisionRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/adventures/{adventure_id}/entries":{"get":{"tags":["api","adventures"],"summary":"List Entries","operationId":"list_entries_api_adventures__adventure_id__entries_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EntryResponse"},"title":"Response List Entries Api Adventures Adventure Id Entries Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/adventures/{adventure_id}/entries/{entry_id}":{"get":{"tags":["api","adventures"],"summary":"Get Entry","operationId":"get_entry_api_adventures__adventure_id__entries__entry_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}},{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntryDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/articles":{"post":{"tags":["api","adventures"],"summary":"Create Article","operationId":"create_article_api_articles_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleBody"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/articles/{article_id}":{"get":{"tags":["api","adventures"],"summary":"Get Article","operationId":"get_article_api_articles__article_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__routers__api__articles__ArticleItem"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/register":{"post":{"tags":["api","auth"],"summary":"Register","operationId":"register_api_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/login":{"post":{"tags":["api","auth"],"summary":"Login","operationId":"login_api_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/verify-email":{"get":{"tags":["api","auth"],"summary":"Verify Email","operationId":"verify_email_api_auth_verify_email_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Verify Email Api Auth Verify Email Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dictionary/search":{"get":{"tags":["api","dictionary"],"summary":"Search Wordforms Prefix","description":"Search for wordforms whose surface text starts with the given prefix.\n\nUses accent-insensitive, case-insensitive prefix matching so that e.g.\n\"chatea\" returns both \"château\" and \"châteaux\", and \"lent\" returns all\nfour forms of the adjective. Returns one entry per matching lemma.","operationId":"search_wordforms_prefix_api_dictionary_search_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"lang_code","in":"query","required":true,"schema":{"type":"string","title":"Lang Code"}},{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WordformMatch"},"title":"Response Search Wordforms Prefix Api Dictionary Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dictionary/senses":{"get":{"tags":["api","dictionary"],"summary":"Search Senses","description":"Search for a Sense by (English) definition\n\nReturns one entry per matching senses,each with its Sense.","operationId":"search_senses_api_dictionary_senses_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"lang_code","in":"query","required":true,"schema":{"type":"string","title":"Lang Code"}},{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SenseMatch"},"title":"Response Search Senses Api Dictionary Senses Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/dictionary/wordforms":{"get":{"tags":["api","dictionary"],"summary":"Search Wordforms","description":"Search for a wordform by surface text within a language.\n\nReturns one entry per matching lemma, each with the lemma's senses. A single\nform (e.g. \"allons\") may resolve to more than one lemma when homographs exist.","operationId":"search_wordforms_api_dictionary_wordforms_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"lang_code","in":"query","required":true,"schema":{"type":"string","title":"Lang Code"}},{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WordformMatch"},"title":"Response Search Wordforms Api Dictionary Wordforms Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/vocab/{entry_id}/flashcards":{"post":{"tags":["api","flashcards"],"summary":"Generate Flashcards","operationId":"generate_flashcards_api_vocab__entry_id__flashcards_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateFlashcardsRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FlashcardResponse"},"title":"Response Generate Flashcards Api Vocab Entry Id Flashcards Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/flashcards":{"get":{"tags":["api","flashcards"],"summary":"List Flashcards","operationId":"list_flashcards_api_flashcards_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/FlashcardResponse"},"type":"array","title":"Response List Flashcards Api Flashcards Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/flashcards/{flashcard_id}/events":{"post":{"tags":["api","flashcards"],"summary":"Record Event","operationId":"record_event_api_flashcards__flashcard_id__events_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"flashcard_id","in":"path","required":true,"schema":{"type":"string","title":"Flashcard Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordEventRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlashcardEventResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/generate":{"post":{"tags":["api","api"],"summary":"Create Generation Job","operationId":"create_generation_job_api_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/jobs/":{"get":{"tags":["api"],"summary":"Get Jobs","operationId":"get_jobs_api_jobs__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobListResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/jobs/{job_id}":{"get":{"tags":["api"],"summary":"Get Job","operationId":"get_job_api_jobs__job_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/learnable_languages":{"post":{"tags":["api","api"],"summary":"Upsert Learnable Language","operationId":"upsert_learnable_language_api_learnable_languages_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearnableLanguageRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearnableLanguageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/packs":{"get":{"tags":["api","packs"],"summary":"List Packs","operationId":"list_packs_api_packs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"source_lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Lang"}},{"name":"target_lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PackSummaryResponse"},"title":"Response List Packs Api Packs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/packs/{pack_id}":{"get":{"tags":["api","packs"],"summary":"Get Pack","operationId":"get_pack_api_packs__pack_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__routers__api__packs__PackDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/packs/{pack_id}/add-to-bank":{"post":{"tags":["api","packs"],"summary":"Add Pack To Bank","operationId":"add_pack_to_bank_api_packs__pack_id__add_to_bank_post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"pack_id","in":"path","required":true,"schema":{"type":"string","title":"Pack Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddTobankRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddTobankResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/pos/":{"post":{"tags":["api","api","pos"],"summary":"Analyze Pos","operationId":"analyze_pos_api_pos__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/POSRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/POSResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/translate":{"get":{"tags":["api","api","translate"],"summary":"Translate text to a target language","operationId":"translate_text_api_translate_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"target_language","in":"query","required":true,"schema":{"type":"string","title":"Target Language"}},{"name":"context","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Context"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/vocab":{"post":{"tags":["api","vocab"],"summary":"Add Word","operationId":"add_word_api_vocab_post","security":[{"HTTPBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddWordRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WordBankEntryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["api","vocab"],"summary":"List Entries","operationId":"list_entries_api_vocab_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"language_pair_id","in":"query","required":true,"schema":{"type":"string","title":"Language Pair Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WordBankEntryResponse"},"title":"Response List Entries Api Vocab Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/vocab/from-token":{"post":{"tags":["api","vocab"],"summary":"Add From Token","operationId":"add_from_token_api_vocab_from_token_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddFromTokenRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FromTokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/vocab/pending-disambiguation":{"get":{"tags":["api","vocab"],"summary":"Pending Disambiguation","operationId":"pending_disambiguation_api_vocab_pending_disambiguation_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/WordBankEntryResponse"},"type":"array","title":"Response Pending Disambiguation Api Vocab Pending Disambiguation Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/vocab/{entry_id}/sense":{"patch":{"tags":["api","vocab"],"summary":"Resolve Sense","operationId":"resolve_sense_api_vocab__entry_id__sense_patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"entry_id","in":"path","required":true,"schema":{"type":"string","title":"Entry Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetSenseRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WordBankEntryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bff/account":{"get":{"tags":["bff","bff"],"summary":"Get Account","operationId":"get_account_bff_account_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/bff/account/onboarding":{"get":{"tags":["bff","bff"],"summary":"Get Onboarding","operationId":"get_onboarding_bff_account_onboarding_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/bff/adventure/{adventure_id}":{"get":{"tags":["bff","bff","adventures"],"summary":"Get Adventure","operationId":"get_adventure_bff_adventure__adventure_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"adventure_id","in":"path","required":true,"schema":{"type":"string","title":"Adventure Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdventureDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bff/articles":{"get":{"tags":["bff","bff","articles"],"summary":"List Articles","operationId":"list_articles_bff_articles_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleListResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/bff/articles/{article_id}":{"get":{"tags":["bff","bff","articles"],"summary":"Get Article","operationId":"get_article_bff_articles__article_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bff/user_profile":{"get":{"tags":["bff","bff"],"summary":"Get User Profile","operationId":"get_user_profile_bff_user_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfileResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/bff/packs":{"get":{"tags":["bff","bff-packs"],"summary":"List Packs For Selection","operationId":"list_packs_for_selection_bff_packs_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"source_lang","in":"query","required":true,"schema":{"type":"string","title":"Source Lang"}},{"name":"target_lang","in":"query","required":true,"schema":{"type":"string","title":"Target Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PackSelectionItem"},"title":"Response List Packs For Selection Bff Packs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/media/adventure-audio/{filename}":{"get":{"tags":["media"],"summary":"Get Adventure Audio File","operationId":"get_adventure_audio_file_media_adventure_audio__filename__get","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/media/{filename}":{"get":{"tags":["media"],"summary":"Get Media File","operationId":"get_media_file_media__filename__get","parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/metrics":{"get":{"summary":"Metrics","description":"Endpoint that serves Prometheus metrics.","operationId":"metrics_metrics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Health Health Get"}}}}}}}},"components":{"schemas":{"AccountLanguagePair":{"properties":{"id":{"type":"string","title":"Id"},"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["id","source_language","target_language","proficiencies"],"title":"AccountLanguagePair"},"AccountResponse":{"properties":{"email":{"type":"string","title":"Email"},"human_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Human Name"},"language_pairs":{"items":{"$ref":"#/components/schemas/AccountLanguagePair"},"type":"array","title":"Language Pairs"}},"type":"object","required":["email","human_name","language_pairs"],"title":"AccountResponse"},"AccountStatusResponse":{"properties":{"problem_flags":{"items":{"type":"string"},"type":"array","title":"Problem Flags"},"error_messages":{"items":{"type":"string"},"type":"array","title":"Error Messages"}},"type":"object","required":["problem_flags","error_messages"],"title":"AccountStatusResponse"},"AddEntryRequest":{"properties":{"sense_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sense Id"},"surface_text":{"type":"string","title":"Surface Text"}},"type":"object","required":["surface_text"],"title":"AddEntryRequest"},"AddFlashcardTemplateRequest":{"properties":{"prompt_text":{"type":"string","title":"Prompt Text"},"answer_text":{"type":"string","title":"Answer Text"},"prompt_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt Context Text"},"answer_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Context Text"}},"type":"object","required":["prompt_text","answer_text"],"title":"AddFlashcardTemplateRequest"},"AddFromTokenRequest":{"properties":{"language_pair_id":{"type":"string","title":"Language Pair Id"},"surface":{"type":"string","title":"Surface"},"spacy_lemma":{"type":"string","title":"Spacy Lemma"},"pos_ud":{"type":"string","title":"Pos Ud"},"language":{"type":"string","title":"Language"},"source_article_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Article Id"}},"type":"object","required":["language_pair_id","surface","spacy_lemma","pos_ud","language"],"title":"AddFromTokenRequest"},"AddLearnableLanguageRequest":{"properties":{"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["source_language","target_language","proficiencies"],"title":"AddLearnableLanguageRequest"},"AddTobankRequest":{"properties":{"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"}},"type":"object","required":["source_lang","target_lang"],"title":"AddTobankRequest"},"AddTobankResponse":{"properties":{"added":{"items":{"type":"string"},"type":"array","title":"Added"}},"type":"object","required":["added"],"title":"AddTobankResponse"},"AddWordRequest":{"properties":{"language_pair_id":{"type":"string","title":"Language Pair Id"},"surface_text":{"type":"string","title":"Surface Text"},"entry_pathway":{"type":"string","title":"Entry Pathway","default":"manual"},"is_phrase":{"type":"boolean","title":"Is Phrase","default":false},"source_article_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Article Id"}},"type":"object","required":["language_pair_id","surface_text"],"title":"AddWordRequest"},"AdventureChoiceItem":{"properties":{"id":{"type":"string","title":"Id"},"index":{"type":"integer","title":"Index"},"label":{"type":"string","title":"Label"},"text":{"type":"string","title":"Text"}},"type":"object","required":["id","index","label","text"],"title":"AdventureChoiceItem"},"AdventureDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"status":{"type":"string","title":"Status"},"language":{"type":"string","title":"Language"},"source_language":{"type":"string","title":"Source Language"},"competencies":{"items":{"type":"string"},"type":"array","title":"Competencies"},"max_entry_count":{"type":"integer","title":"Max Entry Count"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"setting":{"items":{"type":"string"},"type":"array","title":"Setting"},"vibes":{"items":{"type":"string"},"type":"array","title":"Vibes"},"protagonist":{"items":{"type":"string"},"type":"array","title":"Protagonist"},"created_at":{"type":"string","title":"Created At"},"entries":{"items":{"$ref":"#/components/schemas/AdventureEntryItem"},"type":"array","title":"Entries"},"current_entry_choices":{"items":{"$ref":"#/components/schemas/AdventureChoiceItem"},"type":"array","title":"Current Entry Choices"}},"type":"object","required":["id","user_id","status","language","source_language","competencies","max_entry_count","title","description","genres","setting","vibes","protagonist","created_at","entries","current_entry_choices"],"title":"AdventureDetailResponse"},"AdventureEntryItem":{"properties":{"id":{"type":"string","title":"Id"},"adventure_id":{"type":"string","title":"Adventure Id"},"possible_choices":{"anyOf":[{"items":{"$ref":"#/components/schemas/AdventureChoiceItem"},"type":"array"},{"type":"null"}],"title":"Possible Choices"},"generated_from_choice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated From Choice Id"},"status":{"type":"string","title":"Status"},"entry_index":{"type":"integer","title":"Entry Index"},"story_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Story Text"},"story_text_linguistic_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Story Text Linguistic Data"},"translation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Translation"},"audio_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio Url"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","adventure_id","possible_choices","generated_from_choice_id","status","entry_index","story_text","story_text_linguistic_data","translation","audio_url","created_at"],"title":"AdventureEntryItem"},"AdventureResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"status":{"type":"string","title":"Status"},"language":{"type":"string","title":"Language"},"source_language":{"type":"string","title":"Source Language"},"competencies":{"items":{"type":"string"},"type":"array","title":"Competencies"},"max_entry_count":{"type":"integer","title":"Max Entry Count"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"setting":{"items":{"type":"string"},"type":"array","title":"Setting"},"vibes":{"items":{"type":"string"},"type":"array","title":"Vibes"},"protagonist":{"items":{"type":"string"},"type":"array","title":"Protagonist"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","user_id","status","language","source_language","competencies","max_entry_count","title","description","genres","setting","vibes","protagonist","created_at"],"title":"AdventureResponse"},"ArticleDetail":{"properties":{"id":{"type":"string","title":"Id"},"published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Published At"},"language":{"type":"string","title":"Language"},"complexity":{"type":"string","title":"Complexity"},"title":{"type":"string","title":"Title"},"body":{"type":"string","title":"Body"},"audio_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio Url"},"body_pos":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Body Pos"}},"type":"object","required":["id","published_at","language","complexity","title","body","audio_url","body_pos"],"title":"ArticleDetail"},"ArticleListResponse":{"properties":{"articles":{"items":{"$ref":"#/components/schemas/app__routers__bff__articles__ArticleItem"},"type":"array","title":"Articles"}},"type":"object","required":["articles"],"title":"ArticleListResponse"},"ArticleTypeEnum":{"type":"string","enum":["summary"],"title":"ArticleTypeEnum"},"ChoiceResponse":{"properties":{"id":{"type":"string","title":"Id"},"index":{"type":"integer","title":"Index"},"label":{"type":"string","title":"Label"},"text":{"type":"string","title":"Text"}},"type":"object","required":["id","index","label","text"],"title":"ChoiceResponse"},"CreateAdventureRequest":{"properties":{"language":{"type":"string","title":"Language"},"source_language":{"type":"string","title":"Source Language"},"competencies":{"items":{"type":"string"},"type":"array","title":"Competencies"},"genres":{"items":{"type":"string"},"type":"array","title":"Genres"},"setting":{"items":{"type":"string"},"type":"array","title":"Setting"},"vibes":{"items":{"type":"string"},"type":"array","title":"Vibes"},"protagonist":{"items":{"type":"string"},"type":"array","title":"Protagonist"},"entry_word_count_range":{"type":"string","title":"Entry Word Count Range"},"max_entry_count":{"type":"integer","title":"Max Entry Count","default":6}},"type":"object","required":["language","source_language","competencies","genres","setting","vibes","protagonist","entry_word_count_range"],"title":"CreateAdventureRequest"},"CreateArticleBody":{"properties":{"article_type":{"$ref":"#/components/schemas/ArticleTypeEnum"},"language":{"type":"string","title":"Language"},"target_complexity":{"type":"string","title":"Target Complexity"},"text":{"type":"string","title":"Text"}},"type":"object","required":["article_type","language","target_complexity","text"],"title":"CreateArticleBody"},"CreateArticleResponse":{"properties":{"id":{"type":"string","title":"Id"}},"type":"object","required":["id"],"title":"CreateArticleResponse"},"CreateDecisionRequest":{"properties":{"choice_id":{"type":"string","title":"Choice Id"}},"type":"object","required":["choice_id"],"title":"CreateDecisionRequest"},"CreatePackRequest":{"properties":{"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies","default":[]}},"type":"object","required":["name","name_target","description","description_target","source_lang","target_lang"],"title":"CreatePackRequest"},"DecisionResponse":{"properties":{"id":{"type":"string","title":"Id"},"choice_id":{"type":"string","title":"Choice Id"},"user_id":{"type":"string","title":"User Id"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","choice_id","user_id","created_at"],"title":"DecisionResponse"},"EntryDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"adventure_id":{"type":"string","title":"Adventure Id"},"generated_from_choice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated From Choice Id"},"status":{"type":"string","title":"Status"},"entry_index":{"type":"integer","title":"Entry Index"},"story_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Story Text"},"created_at":{"type":"string","title":"Created At"},"choices":{"items":{"$ref":"#/components/schemas/ChoiceResponse"},"type":"array","title":"Choices"},"translation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Translation"},"audio_file_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio File Name"},"story_text_linguistic_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Story Text Linguistic Data"}},"type":"object","required":["id","adventure_id","generated_from_choice_id","status","entry_index","story_text","created_at","choices","translation","audio_file_name","story_text_linguistic_data"],"title":"EntryDetailResponse"},"EntryResponse":{"properties":{"id":{"type":"string","title":"Id"},"adventure_id":{"type":"string","title":"Adventure Id"},"generated_from_choice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated From Choice Id"},"status":{"type":"string","title":"Status"},"entry_index":{"type":"integer","title":"Entry Index"},"story_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Story Text"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","adventure_id","generated_from_choice_id","status","entry_index","story_text","created_at"],"title":"EntryResponse"},"FlashcardEventResponse":{"properties":{"id":{"type":"string","title":"Id"},"flashcard_id":{"type":"string","title":"Flashcard Id"},"user_id":{"type":"string","title":"User Id"},"event_type":{"type":"string","title":"Event Type"},"user_response":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Response"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","flashcard_id","user_id","event_type","user_response","created_at"],"title":"FlashcardEventResponse"},"FlashcardResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"bank_entry_id":{"type":"string","title":"Bank Entry Id"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"prompt_text":{"type":"string","title":"Prompt Text"},"answer_text":{"type":"string","title":"Answer Text"},"prompt_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt Context Text"},"answer_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Context Text"},"prompt_modality":{"type":"string","title":"Prompt Modality"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","user_id","bank_entry_id","source_lang","target_lang","prompt_text","answer_text","prompt_context_text","answer_context_text","prompt_modality","created_at"],"title":"FlashcardResponse"},"FlashcardTemplateResponse":{"properties":{"id":{"type":"string","title":"Id"},"pack_entry_id":{"type":"string","title":"Pack Entry Id"},"prompt_text":{"type":"string","title":"Prompt Text"},"answer_text":{"type":"string","title":"Answer Text"},"prompt_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt Context Text"},"answer_context_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Context Text"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","pack_entry_id","prompt_text","answer_text","prompt_context_text","answer_context_text","created_at"],"title":"FlashcardTemplateResponse"},"FromTokenResponse":{"properties":{"entry":{"$ref":"#/components/schemas/WordBankEntryResponse"},"sense_candidates":{"items":{"$ref":"#/components/schemas/SenseCandidateResponse"},"type":"array","title":"Sense Candidates"},"matched_via":{"type":"string","title":"Matched Via"}},"type":"object","required":["entry","sense_candidates","matched_via"],"title":"FromTokenResponse"},"GenerateFlashcardsRequest":{"properties":{"direction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Direction"}},"type":"object","title":"GenerateFlashcardsRequest"},"GenerationRequest":{"properties":{"target_language":{"type":"string","title":"Target Language"},"complexity_level":{"type":"string","title":"Complexity Level"},"text":{"type":"string","title":"Text"},"source_language":{"type":"string","title":"Source Language","default":"en"}},"type":"object","required":["target_language","complexity_level","text"],"title":"GenerationRequest"},"GenerationResponse":{"properties":{"article_id":{"type":"string","title":"Article Id"}},"type":"object","required":["article_id"],"title":"GenerationResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"JobListResponse":{"properties":{"jobs":{"items":{"$ref":"#/components/schemas/JobSummary"},"type":"array","title":"Jobs"}},"type":"object","required":["jobs"],"title":"JobListResponse"},"JobResponse":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"status":{"type":"string","title":"Status"},"translated_article_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Translated Article Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Started At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"}},"type":"object","required":["id","status","created_at"],"title":"JobResponse"},"JobSummary":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"}},"type":"object","required":["id","status","created_at"],"title":"JobSummary"},"LanguagePairOption":{"properties":{"value":{"type":"string","title":"Value"},"label":{"type":"string","title":"Label"},"description":{"type":"string","title":"Description"}},"type":"object","required":["value","label","description"],"title":"LanguagePairOption"},"LearnableLanguageItem":{"properties":{"id":{"type":"string","title":"Id"},"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["id","source_language","target_language","proficiencies"],"title":"LearnableLanguageItem"},"LearnableLanguageRequest":{"properties":{"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["source_language","target_language","proficiencies"],"title":"LearnableLanguageRequest"},"LearnableLanguageResponse":{"properties":{"id":{"type":"string","title":"Id"},"source_language":{"type":"string","title":"Source Language"},"target_language":{"type":"string","title":"Target Language"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"}},"type":"object","required":["id","source_language","target_language","proficiencies"],"title":"LearnableLanguageResponse"},"LemmaResponse":{"properties":{"id":{"type":"string","title":"Id"},"headword":{"type":"string","title":"Headword"},"language":{"type":"string","title":"Language"},"pos_raw":{"type":"string","title":"Pos Raw"},"pos_normalised":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pos Normalised"},"gender":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gender"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"}},"type":"object","required":["id","headword","language","pos_raw","pos_normalised","gender","tags"],"title":"LemmaResponse"},"LoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"LoginRequest"},"OnboardingRequest":{"properties":{"human_name":{"type":"string","title":"Human Name"},"language_pairs":{"items":{"type":"string"},"type":"array","title":"Language Pairs"},"proficiencies":{"items":{"items":{"type":"string"},"type":"array"},"type":"array","title":"Proficiencies"}},"type":"object","required":["human_name","language_pairs","proficiencies"],"title":"OnboardingRequest"},"OnboardingResponse":{"properties":{"language_pairs":{"items":{"$ref":"#/components/schemas/LanguagePairOption"},"type":"array","title":"Language Pairs"},"proficiencies":{"items":{"$ref":"#/components/schemas/ProficiencyOption"},"type":"array","title":"Proficiencies"}},"type":"object","required":["language_pairs","proficiencies"],"title":"OnboardingResponse"},"POSRequest":{"properties":{"text":{"type":"string","title":"Text"},"language":{"type":"string","title":"Language"}},"type":"object","required":["text","language"],"title":"POSRequest"},"POSResponse":{"properties":{"language":{"type":"string","title":"Language"},"tokens":{"items":{"$ref":"#/components/schemas/TokenInfo"},"type":"array","title":"Tokens"}},"type":"object","required":["language","tokens"],"title":"POSResponse"},"PackEntryResponse":{"properties":{"id":{"type":"string","title":"Id"},"pack_id":{"type":"string","title":"Pack Id"},"sense_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sense Id"},"surface_text":{"type":"string","title":"Surface Text"},"created_at":{"type":"string","title":"Created At"},"flashcard_templates":{"items":{"$ref":"#/components/schemas/FlashcardTemplateResponse"},"type":"array","title":"Flashcard Templates","default":[]}},"type":"object","required":["id","pack_id","sense_id","surface_text","created_at"],"title":"PackEntryResponse"},"PackResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"is_published":{"type":"boolean","title":"Is Published"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","is_published","created_at"],"title":"PackResponse"},"PackSelectionItem":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"entry_count":{"type":"integer","title":"Entry Count"},"already_added":{"type":"boolean","title":"Already Added"}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","entry_count","already_added"],"title":"PackSelectionItem"},"PackSummaryResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"entry_count":{"type":"integer","title":"Entry Count"}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","entry_count"],"title":"PackSummaryResponse"},"ProficiencyOption":{"properties":{"value":{"type":"string","title":"Value"},"label":{"type":"string","title":"Label"},"description":{"type":"string","title":"Description"}},"type":"object","required":["value","label","description"],"title":"ProficiencyOption"},"RecordEventRequest":{"properties":{"event_type":{"type":"string","title":"Event Type"},"user_response":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Response"}},"type":"object","required":["event_type"],"title":"RecordEventRequest"},"RegisterRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"RegisterRequest"},"RegisterResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"}},"type":"object","required":["success"],"title":"RegisterResponse"},"SenseCandidateResponse":{"properties":{"id":{"type":"string","title":"Id"},"gloss":{"type":"string","title":"Gloss"},"topics":{"items":{"type":"string"},"type":"array","title":"Topics"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"}},"type":"object","required":["id","gloss","topics","tags"],"title":"SenseCandidateResponse"},"SenseMatch":{"properties":{"sense":{"$ref":"#/components/schemas/SenseResponse"},"lemma":{"$ref":"#/components/schemas/LemmaResponse"}},"type":"object","required":["sense","lemma"],"title":"SenseMatch"},"SenseResponse":{"properties":{"id":{"type":"string","title":"Id"},"sense_index":{"type":"integer","title":"Sense Index"},"gloss":{"type":"string","title":"Gloss"},"topics":{"items":{"type":"string"},"type":"array","title":"Topics"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"}},"type":"object","required":["id","sense_index","gloss","topics","tags"],"title":"SenseResponse"},"SetSenseRequest":{"properties":{"sense_id":{"type":"string","title":"Sense Id"}},"type":"object","required":["sense_id"],"title":"SetSenseRequest"},"TokenInfo":{"properties":{"text":{"type":"string","title":"Text"},"lemma":{"type":"string","title":"Lemma"},"pos":{"type":"string","title":"Pos"},"tag":{"type":"string","title":"Tag"},"dep":{"type":"string","title":"Dep"},"is_stop":{"type":"boolean","title":"Is Stop"}},"type":"object","required":["text","lemma","pos","tag","dep","is_stop"],"title":"TokenInfo"},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type","default":"bearer"}},"type":"object","required":["access_token"],"title":"TokenResponse"},"TranslationResponse":{"properties":{"text":{"type":"string","title":"Text"},"target_language":{"type":"string","title":"Target Language"},"translated_text":{"type":"string","title":"Translated Text"}},"type":"object","required":["text","target_language","translated_text"],"title":"TranslationResponse"},"UpdatePackRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"name_target":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name Target"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"description_target":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description Target"},"proficiencies":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Proficiencies"}},"type":"object","title":"UpdatePackRequest"},"UserProfileResponse":{"properties":{"learnable_languages":{"items":{"$ref":"#/components/schemas/LearnableLanguageItem"},"type":"array","title":"Learnable Languages"}},"type":"object","required":["learnable_languages"],"title":"UserProfileResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WordBankEntryResponse":{"properties":{"id":{"type":"string","title":"Id"},"user_id":{"type":"string","title":"User Id"},"language_pair_id":{"type":"string","title":"Language Pair Id"},"sense_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sense Id"},"wordform_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wordform Id"},"surface_text":{"type":"string","title":"Surface Text"},"is_phrase":{"type":"boolean","title":"Is Phrase"},"entry_pathway":{"type":"string","title":"Entry Pathway"},"source_article_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Article Id"},"disambiguation_status":{"type":"string","title":"Disambiguation Status"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","user_id","language_pair_id","sense_id","wordform_id","surface_text","is_phrase","entry_pathway","source_article_id","disambiguation_status","created_at"],"title":"WordBankEntryResponse"},"WordformMatch":{"properties":{"lemma":{"$ref":"#/components/schemas/LemmaResponse"},"senses":{"items":{"$ref":"#/components/schemas/SenseResponse"},"type":"array","title":"Senses"}},"type":"object","required":["lemma","senses"],"title":"WordformMatch"},"app__routers__api__admin__packs__PackDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"is_published":{"type":"boolean","title":"Is Published"},"created_at":{"type":"string","title":"Created At"},"entries":{"items":{"$ref":"#/components/schemas/PackEntryResponse"},"type":"array","title":"Entries","default":[]}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","is_published","created_at"],"title":"PackDetailResponse"},"app__routers__api__articles__ArticleItem":{"properties":{"id":{"type":"string","title":"Id"}},"type":"object","required":["id"],"title":"ArticleItem"},"app__routers__api__packs__PackDetailResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"name_target":{"type":"string","title":"Name Target"},"description":{"type":"string","title":"Description"},"description_target":{"type":"string","title":"Description Target"},"source_lang":{"type":"string","title":"Source Lang"},"target_lang":{"type":"string","title":"Target Lang"},"proficiencies":{"items":{"type":"string"},"type":"array","title":"Proficiencies"},"entry_count":{"type":"integer","title":"Entry Count"},"surface_texts":{"items":{"type":"string"},"type":"array","title":"Surface Texts"}},"type":"object","required":["id","name","name_target","description","description_target","source_lang","target_lang","proficiencies","entry_count","surface_texts"],"title":"PackDetailResponse"},"app__routers__bff__articles__ArticleItem":{"properties":{"id":{"type":"string","title":"Id"},"published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Published At"},"language":{"type":"string","title":"Language"},"title":{"type":"string","title":"Title"},"complexity":{"type":"string","title":"Complexity"}},"type":"object","required":["id","published_at","language","title","complexity"],"title":"ArticleItem"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}} \ No newline at end of file diff --git a/frontend/src/client/index.ts b/frontend/src/client/index.ts index 453fe54..dc4259d 100644 --- a/frontend/src/client/index.ts +++ b/frontend/src/client/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts export { addEntryApiAdminPacksPackIdEntriesPost, addFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPost, addFromTokenApiVocabFromTokenPost, addLearnableLanguageApiAccountLearnableLanguagesPost, addPackToBankApiPacksPackIdAddToBankPost, addWordApiVocabPost, analyzePosApiPosPost, completeOnboardingApiAccountOnboardingPost, createAdventureApiAdventuresPost, createArticleApiArticlesPost, createGenerationJobApiGeneratePost, createPackApiAdminPacksPost, deleteAdventureApiAdventuresAdventureIdDelete, generateFlashcardsApiVocabEntryIdFlashcardsPost, getAccountBffAccountGet, getAccountStatusApiAccountStatusGet, getAdventureApiAdventuresAdventureIdGet, getAdventureAudioFileMediaAdventureAudioFilenameGet, getAdventureBffAdventureAdventureIdGet, getArticleApiArticlesArticleIdGet, getArticleBffArticlesArticleIdGet, getEntryApiAdventuresAdventureIdEntriesEntryIdGet, getJobApiJobsJobIdGet, getJobsApiJobsGet, getMediaFileMediaFilenameGet, getOnboardingBffAccountOnboardingGet, getPackApiAdminPacksPackIdGet, getPackApiPacksPackIdGet, getUserProfileBffUserProfileGet, healthHealthGet, listAdventuresApiAdventuresGet, listArticlesBffArticlesGet, listEntriesApiAdventuresAdventureIdEntriesGet, listEntriesApiVocabGet, listFlashcardsApiFlashcardsGet, listPacksApiAdminPacksGet, listPacksApiPacksGet, listPacksForSelectionBffPacksGet, loginApiAuthLoginPost, metricsMetricsGet, type Options, pendingDisambiguationApiVocabPendingDisambiguationGet, publishPackApiAdminPacksPackIdPublishPost, recordDecisionApiAdventuresAdventureIdDecisionsPost, recordEventApiFlashcardsFlashcardIdEventsPost, registerApiAuthRegisterPost, removeEntryApiAdminPacksPackIdEntriesEntryIdDelete, removeFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDelete, removeLearnableLanguageApiAccountLearnableLanguagesLanguageIdDelete, resolveSenseApiVocabEntryIdSensePatch, searchSensesApiDictionarySensesGet, searchWordformsApiDictionaryWordformsGet, searchWordformsPrefixApiDictionarySearchGet, translateTextApiTranslateGet, updatePackApiAdminPacksPackIdPatch, upsertLearnableLanguageApiLearnableLanguagesPost, verifyEmailApiAuthVerifyEmailGet } from './sdk.gen'; -export type { AccountLanguagePair, AccountResponse, AccountStatusResponse, AddEntryApiAdminPacksPackIdEntriesPostData, AddEntryApiAdminPacksPackIdEntriesPostError, AddEntryApiAdminPacksPackIdEntriesPostErrors, AddEntryApiAdminPacksPackIdEntriesPostResponse, AddEntryApiAdminPacksPackIdEntriesPostResponses, AddEntryRequest, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostData, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostError, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostErrors, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostResponse, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostResponses, AddFlashcardTemplateRequest, AddFromTokenApiVocabFromTokenPostData, AddFromTokenApiVocabFromTokenPostError, AddFromTokenApiVocabFromTokenPostErrors, AddFromTokenApiVocabFromTokenPostResponse, AddFromTokenApiVocabFromTokenPostResponses, AddFromTokenRequest, AddLearnableLanguageApiAccountLearnableLanguagesPostData, AddLearnableLanguageApiAccountLearnableLanguagesPostError, AddLearnableLanguageApiAccountLearnableLanguagesPostErrors, AddLearnableLanguageApiAccountLearnableLanguagesPostResponse, AddLearnableLanguageApiAccountLearnableLanguagesPostResponses, AddLearnableLanguageRequest, AddPackToBankApiPacksPackIdAddToBankPostData, AddPackToBankApiPacksPackIdAddToBankPostError, AddPackToBankApiPacksPackIdAddToBankPostErrors, AddPackToBankApiPacksPackIdAddToBankPostResponse, AddPackToBankApiPacksPackIdAddToBankPostResponses, AddTobankRequest, AddTobankResponse, AddWordApiVocabPostData, AddWordApiVocabPostError, AddWordApiVocabPostErrors, AddWordApiVocabPostResponse, AddWordApiVocabPostResponses, AddWordRequest, AdventureChoiceItem, AdventureDetailResponse, AdventureEntryItem, AdventureResponse, AnalyzePosApiPosPostData, AnalyzePosApiPosPostError, AnalyzePosApiPosPostErrors, AnalyzePosApiPosPostResponse, AnalyzePosApiPosPostResponses, AppRoutersApiAdminPacksPackDetailResponse, AppRoutersApiArticlesArticleItem, AppRoutersApiPacksPackDetailResponse, AppRoutersBffArticlesArticleItem, ArticleDetail, ArticleListResponse, ArticleTypeEnum, ChoiceResponse, ClientOptions, CompleteOnboardingApiAccountOnboardingPostData, CompleteOnboardingApiAccountOnboardingPostError, CompleteOnboardingApiAccountOnboardingPostErrors, CompleteOnboardingApiAccountOnboardingPostResponse, CompleteOnboardingApiAccountOnboardingPostResponses, CreateAdventureApiAdventuresPostData, CreateAdventureApiAdventuresPostError, CreateAdventureApiAdventuresPostErrors, CreateAdventureApiAdventuresPostResponse, CreateAdventureApiAdventuresPostResponses, CreateAdventureRequest, CreateArticleApiArticlesPostData, CreateArticleApiArticlesPostError, CreateArticleApiArticlesPostErrors, CreateArticleApiArticlesPostResponse, CreateArticleApiArticlesPostResponses, CreateArticleBody, CreateArticleResponse, CreateDecisionRequest, CreateGenerationJobApiGeneratePostData, CreateGenerationJobApiGeneratePostError, CreateGenerationJobApiGeneratePostErrors, CreateGenerationJobApiGeneratePostResponse, CreateGenerationJobApiGeneratePostResponses, CreatePackApiAdminPacksPostData, CreatePackApiAdminPacksPostError, CreatePackApiAdminPacksPostErrors, CreatePackApiAdminPacksPostResponse, CreatePackApiAdminPacksPostResponses, CreatePackRequest, DecisionResponse, DeleteAdventureApiAdventuresAdventureIdDeleteData, DeleteAdventureApiAdventuresAdventureIdDeleteError, DeleteAdventureApiAdventuresAdventureIdDeleteErrors, DeleteAdventureApiAdventuresAdventureIdDeleteResponse, DeleteAdventureApiAdventuresAdventureIdDeleteResponses, EntryDetailResponse, EntryResponse, FlashcardEventResponse, FlashcardResponse, FlashcardTemplateResponse, FromTokenResponse, GenerateFlashcardsApiVocabEntryIdFlashcardsPostData, GenerateFlashcardsApiVocabEntryIdFlashcardsPostError, GenerateFlashcardsApiVocabEntryIdFlashcardsPostErrors, GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponse, GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponses, GenerateFlashcardsRequest, GenerationRequest, GenerationResponse, GetAccountBffAccountGetData, GetAccountBffAccountGetResponse, GetAccountBffAccountGetResponses, GetAccountStatusApiAccountStatusGetData, GetAccountStatusApiAccountStatusGetResponse, GetAccountStatusApiAccountStatusGetResponses, GetAdventureApiAdventuresAdventureIdGetData, GetAdventureApiAdventuresAdventureIdGetError, GetAdventureApiAdventuresAdventureIdGetErrors, GetAdventureApiAdventuresAdventureIdGetResponse, GetAdventureApiAdventuresAdventureIdGetResponses, GetAdventureAudioFileMediaAdventureAudioFilenameGetData, GetAdventureAudioFileMediaAdventureAudioFilenameGetError, GetAdventureAudioFileMediaAdventureAudioFilenameGetErrors, GetAdventureAudioFileMediaAdventureAudioFilenameGetResponses, GetAdventureBffAdventureAdventureIdGetData, GetAdventureBffAdventureAdventureIdGetError, GetAdventureBffAdventureAdventureIdGetErrors, GetAdventureBffAdventureAdventureIdGetResponse, GetAdventureBffAdventureAdventureIdGetResponses, GetArticleApiArticlesArticleIdGetData, GetArticleApiArticlesArticleIdGetError, GetArticleApiArticlesArticleIdGetErrors, GetArticleApiArticlesArticleIdGetResponse, GetArticleApiArticlesArticleIdGetResponses, GetArticleBffArticlesArticleIdGetData, GetArticleBffArticlesArticleIdGetError, GetArticleBffArticlesArticleIdGetErrors, GetArticleBffArticlesArticleIdGetResponse, GetArticleBffArticlesArticleIdGetResponses, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetData, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetError, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetErrors, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponse, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponses, GetJobApiJobsJobIdGetData, GetJobApiJobsJobIdGetError, GetJobApiJobsJobIdGetErrors, GetJobApiJobsJobIdGetResponse, GetJobApiJobsJobIdGetResponses, GetJobsApiJobsGetData, GetJobsApiJobsGetResponse, GetJobsApiJobsGetResponses, GetMediaFileMediaFilenameGetData, GetMediaFileMediaFilenameGetError, GetMediaFileMediaFilenameGetErrors, GetMediaFileMediaFilenameGetResponses, GetOnboardingBffAccountOnboardingGetData, GetOnboardingBffAccountOnboardingGetResponse, GetOnboardingBffAccountOnboardingGetResponses, GetPackApiAdminPacksPackIdGetData, GetPackApiAdminPacksPackIdGetError, GetPackApiAdminPacksPackIdGetErrors, GetPackApiAdminPacksPackIdGetResponse, GetPackApiAdminPacksPackIdGetResponses, GetPackApiPacksPackIdGetData, GetPackApiPacksPackIdGetError, GetPackApiPacksPackIdGetErrors, GetPackApiPacksPackIdGetResponse, GetPackApiPacksPackIdGetResponses, GetUserProfileBffUserProfileGetData, GetUserProfileBffUserProfileGetResponse, GetUserProfileBffUserProfileGetResponses, HealthHealthGetData, HealthHealthGetResponse, HealthHealthGetResponses, HttpValidationError, JobListResponse, JobResponse, JobSummary, LanguagePairOption, LearnableLanguageItem, LearnableLanguageRequest, LearnableLanguageResponse, LemmaResponse, ListAdventuresApiAdventuresGetData, ListAdventuresApiAdventuresGetResponse, ListAdventuresApiAdventuresGetResponses, ListArticlesBffArticlesGetData, ListArticlesBffArticlesGetError, ListArticlesBffArticlesGetErrors, ListArticlesBffArticlesGetResponse, ListArticlesBffArticlesGetResponses, ListEntriesApiAdventuresAdventureIdEntriesGetData, ListEntriesApiAdventuresAdventureIdEntriesGetError, ListEntriesApiAdventuresAdventureIdEntriesGetErrors, ListEntriesApiAdventuresAdventureIdEntriesGetResponse, ListEntriesApiAdventuresAdventureIdEntriesGetResponses, ListEntriesApiVocabGetData, ListEntriesApiVocabGetError, ListEntriesApiVocabGetErrors, ListEntriesApiVocabGetResponse, ListEntriesApiVocabGetResponses, ListFlashcardsApiFlashcardsGetData, ListFlashcardsApiFlashcardsGetResponse, ListFlashcardsApiFlashcardsGetResponses, ListPacksApiAdminPacksGetData, ListPacksApiAdminPacksGetError, ListPacksApiAdminPacksGetErrors, ListPacksApiAdminPacksGetResponse, ListPacksApiAdminPacksGetResponses, ListPacksApiPacksGetData, ListPacksApiPacksGetError, ListPacksApiPacksGetErrors, ListPacksApiPacksGetResponse, ListPacksApiPacksGetResponses, ListPacksForSelectionBffPacksGetData, ListPacksForSelectionBffPacksGetError, ListPacksForSelectionBffPacksGetErrors, ListPacksForSelectionBffPacksGetResponse, ListPacksForSelectionBffPacksGetResponses, LoginApiAuthLoginPostData, LoginApiAuthLoginPostError, LoginApiAuthLoginPostErrors, LoginApiAuthLoginPostResponse, LoginApiAuthLoginPostResponses, LoginRequest, MetricsMetricsGetData, MetricsMetricsGetResponses, OnboardingRequest, OnboardingResponse, PackEntryResponse, PackResponse, PackSelectionItem, PackSummaryResponse, PendingDisambiguationApiVocabPendingDisambiguationGetData, PendingDisambiguationApiVocabPendingDisambiguationGetResponse, PendingDisambiguationApiVocabPendingDisambiguationGetResponses, PosRequest, PosResponse, ProficiencyOption, PublishPackApiAdminPacksPackIdPublishPostData, PublishPackApiAdminPacksPackIdPublishPostError, PublishPackApiAdminPacksPackIdPublishPostErrors, PublishPackApiAdminPacksPackIdPublishPostResponse, PublishPackApiAdminPacksPackIdPublishPostResponses, RecordDecisionApiAdventuresAdventureIdDecisionsPostData, RecordDecisionApiAdventuresAdventureIdDecisionsPostError, RecordDecisionApiAdventuresAdventureIdDecisionsPostErrors, RecordDecisionApiAdventuresAdventureIdDecisionsPostResponse, RecordDecisionApiAdventuresAdventureIdDecisionsPostResponses, RecordEventApiFlashcardsFlashcardIdEventsPostData, RecordEventApiFlashcardsFlashcardIdEventsPostError, RecordEventApiFlashcardsFlashcardIdEventsPostErrors, RecordEventApiFlashcardsFlashcardIdEventsPostResponse, RecordEventApiFlashcardsFlashcardIdEventsPostResponses, RecordEventRequest, RegisterApiAuthRegisterPostData, RegisterApiAuthRegisterPostError, RegisterApiAuthRegisterPostErrors, RegisterApiAuthRegisterPostResponse, RegisterApiAuthRegisterPostResponses, RegisterRequest, RegisterResponse, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteData, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteError, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteErrors, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteResponse, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteResponses, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteData, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteError, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteErrors, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponse, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponses, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteData, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteError, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteErrors, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponse, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponses, ResolveSenseApiVocabEntryIdSensePatchData, ResolveSenseApiVocabEntryIdSensePatchError, ResolveSenseApiVocabEntryIdSensePatchErrors, ResolveSenseApiVocabEntryIdSensePatchResponse, ResolveSenseApiVocabEntryIdSensePatchResponses, SearchSensesApiDictionarySensesGetData, SearchSensesApiDictionarySensesGetError, SearchSensesApiDictionarySensesGetErrors, SearchSensesApiDictionarySensesGetResponse, SearchSensesApiDictionarySensesGetResponses, SearchWordformsApiDictionaryWordformsGetData, SearchWordformsApiDictionaryWordformsGetError, SearchWordformsApiDictionaryWordformsGetErrors, SearchWordformsApiDictionaryWordformsGetResponse, SearchWordformsApiDictionaryWordformsGetResponses, SearchWordformsPrefixApiDictionarySearchGetData, SearchWordformsPrefixApiDictionarySearchGetError, SearchWordformsPrefixApiDictionarySearchGetErrors, SearchWordformsPrefixApiDictionarySearchGetResponse, SearchWordformsPrefixApiDictionarySearchGetResponses, SenseCandidateResponse, SenseMatch, SenseResponse, SetSenseRequest, TokenInfo, TokenResponse, TranslateTextApiTranslateGetData, TranslateTextApiTranslateGetError, TranslateTextApiTranslateGetErrors, TranslateTextApiTranslateGetResponse, TranslateTextApiTranslateGetResponses, TranslationResponse, UpdatePackApiAdminPacksPackIdPatchData, UpdatePackApiAdminPacksPackIdPatchError, UpdatePackApiAdminPacksPackIdPatchErrors, UpdatePackApiAdminPacksPackIdPatchResponse, UpdatePackApiAdminPacksPackIdPatchResponses, UpdatePackRequest, UpsertLearnableLanguageApiLearnableLanguagesPostData, UpsertLearnableLanguageApiLearnableLanguagesPostError, UpsertLearnableLanguageApiLearnableLanguagesPostErrors, UpsertLearnableLanguageApiLearnableLanguagesPostResponse, UpsertLearnableLanguageApiLearnableLanguagesPostResponses, UserProfileResponse, ValidationError, VerifyEmailApiAuthVerifyEmailGetData, VerifyEmailApiAuthVerifyEmailGetError, VerifyEmailApiAuthVerifyEmailGetErrors, VerifyEmailApiAuthVerifyEmailGetResponse, VerifyEmailApiAuthVerifyEmailGetResponses, WordBankEntryResponse, WordformMatch } from './types.gen'; +export type { AccountLanguagePair, AccountResponse, AccountStatusResponse, AddEntryApiAdminPacksPackIdEntriesPostData, AddEntryApiAdminPacksPackIdEntriesPostError, AddEntryApiAdminPacksPackIdEntriesPostErrors, AddEntryApiAdminPacksPackIdEntriesPostResponse, AddEntryApiAdminPacksPackIdEntriesPostResponses, AddEntryRequest, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostData, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostError, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostErrors, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostResponse, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostResponses, AddFlashcardTemplateRequest, AddFromTokenApiVocabFromTokenPostData, AddFromTokenApiVocabFromTokenPostError, AddFromTokenApiVocabFromTokenPostErrors, AddFromTokenApiVocabFromTokenPostResponse, AddFromTokenApiVocabFromTokenPostResponses, AddFromTokenRequest, AddLearnableLanguageApiAccountLearnableLanguagesPostData, AddLearnableLanguageApiAccountLearnableLanguagesPostError, AddLearnableLanguageApiAccountLearnableLanguagesPostErrors, AddLearnableLanguageApiAccountLearnableLanguagesPostResponse, AddLearnableLanguageApiAccountLearnableLanguagesPostResponses, AddLearnableLanguageRequest, AddPackToBankApiPacksPackIdAddToBankPostData, AddPackToBankApiPacksPackIdAddToBankPostError, AddPackToBankApiPacksPackIdAddToBankPostErrors, AddPackToBankApiPacksPackIdAddToBankPostResponse, AddPackToBankApiPacksPackIdAddToBankPostResponses, AddTobankRequest, AddTobankResponse, AddWordApiVocabPostData, AddWordApiVocabPostError, AddWordApiVocabPostErrors, AddWordApiVocabPostResponse, AddWordApiVocabPostResponses, AddWordRequest, AdventureChoiceItem, AdventureDetailResponse, AdventureEntryItem, AdventureResponse, AnalyzePosApiPosPostData, AnalyzePosApiPosPostError, AnalyzePosApiPosPostErrors, AnalyzePosApiPosPostResponse, AnalyzePosApiPosPostResponses, AppRoutersApiAdminPacksPackDetailResponse, AppRoutersApiArticlesArticleItem, AppRoutersApiPacksPackDetailResponse, AppRoutersBffArticlesArticleItem, ArticleDetail, ArticleListResponse, ArticleTypeEnum, ChoiceResponse, ClientOptions, CompleteOnboardingApiAccountOnboardingPostData, CompleteOnboardingApiAccountOnboardingPostError, CompleteOnboardingApiAccountOnboardingPostErrors, CompleteOnboardingApiAccountOnboardingPostResponse, CompleteOnboardingApiAccountOnboardingPostResponses, CreateAdventureApiAdventuresPostData, CreateAdventureApiAdventuresPostError, CreateAdventureApiAdventuresPostErrors, CreateAdventureApiAdventuresPostResponse, CreateAdventureApiAdventuresPostResponses, CreateAdventureRequest, CreateArticleApiArticlesPostData, CreateArticleApiArticlesPostError, CreateArticleApiArticlesPostErrors, CreateArticleApiArticlesPostResponse, CreateArticleApiArticlesPostResponses, CreateArticleBody, CreateArticleResponse, CreateDecisionRequest, CreateGenerationJobApiGeneratePostData, CreateGenerationJobApiGeneratePostError, CreateGenerationJobApiGeneratePostErrors, CreateGenerationJobApiGeneratePostResponse, CreateGenerationJobApiGeneratePostResponses, CreatePackApiAdminPacksPostData, CreatePackApiAdminPacksPostError, CreatePackApiAdminPacksPostErrors, CreatePackApiAdminPacksPostResponse, CreatePackApiAdminPacksPostResponses, CreatePackRequest, DecisionResponse, DeleteAdventureApiAdventuresAdventureIdDeleteData, DeleteAdventureApiAdventuresAdventureIdDeleteError, DeleteAdventureApiAdventuresAdventureIdDeleteErrors, DeleteAdventureApiAdventuresAdventureIdDeleteResponse, DeleteAdventureApiAdventuresAdventureIdDeleteResponses, EntryDetailResponse, EntryResponse, FlashcardEventResponse, FlashcardResponse, FlashcardTemplateResponse, FromTokenResponse, GenerateFlashcardsApiVocabEntryIdFlashcardsPostData, GenerateFlashcardsApiVocabEntryIdFlashcardsPostError, GenerateFlashcardsApiVocabEntryIdFlashcardsPostErrors, GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponse, GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponses, GenerateFlashcardsRequest, GenerationRequest, GenerationResponse, GetAccountBffAccountGetData, GetAccountBffAccountGetResponse, GetAccountBffAccountGetResponses, GetAccountStatusApiAccountStatusGetData, GetAccountStatusApiAccountStatusGetResponse, GetAccountStatusApiAccountStatusGetResponses, GetAdventureApiAdventuresAdventureIdGetData, GetAdventureApiAdventuresAdventureIdGetError, GetAdventureApiAdventuresAdventureIdGetErrors, GetAdventureApiAdventuresAdventureIdGetResponse, GetAdventureApiAdventuresAdventureIdGetResponses, GetAdventureAudioFileMediaAdventureAudioFilenameGetData, GetAdventureAudioFileMediaAdventureAudioFilenameGetError, GetAdventureAudioFileMediaAdventureAudioFilenameGetErrors, GetAdventureAudioFileMediaAdventureAudioFilenameGetResponses, GetAdventureBffAdventureAdventureIdGetData, GetAdventureBffAdventureAdventureIdGetError, GetAdventureBffAdventureAdventureIdGetErrors, GetAdventureBffAdventureAdventureIdGetResponse, GetAdventureBffAdventureAdventureIdGetResponses, GetArticleApiArticlesArticleIdGetData, GetArticleApiArticlesArticleIdGetError, GetArticleApiArticlesArticleIdGetErrors, GetArticleApiArticlesArticleIdGetResponse, GetArticleApiArticlesArticleIdGetResponses, GetArticleBffArticlesArticleIdGetData, GetArticleBffArticlesArticleIdGetError, GetArticleBffArticlesArticleIdGetErrors, GetArticleBffArticlesArticleIdGetResponse, GetArticleBffArticlesArticleIdGetResponses, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetData, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetError, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetErrors, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponse, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponses, GetJobApiJobsJobIdGetData, GetJobApiJobsJobIdGetError, GetJobApiJobsJobIdGetErrors, GetJobApiJobsJobIdGetResponse, GetJobApiJobsJobIdGetResponses, GetJobsApiJobsGetData, GetJobsApiJobsGetResponse, GetJobsApiJobsGetResponses, GetMediaFileMediaFilenameGetData, GetMediaFileMediaFilenameGetError, GetMediaFileMediaFilenameGetErrors, GetMediaFileMediaFilenameGetResponses, GetOnboardingBffAccountOnboardingGetData, GetOnboardingBffAccountOnboardingGetResponse, GetOnboardingBffAccountOnboardingGetResponses, GetPackApiAdminPacksPackIdGetData, GetPackApiAdminPacksPackIdGetError, GetPackApiAdminPacksPackIdGetErrors, GetPackApiAdminPacksPackIdGetResponse, GetPackApiAdminPacksPackIdGetResponses, GetPackApiPacksPackIdGetData, GetPackApiPacksPackIdGetError, GetPackApiPacksPackIdGetErrors, GetPackApiPacksPackIdGetResponse, GetPackApiPacksPackIdGetResponses, GetUserProfileBffUserProfileGetData, GetUserProfileBffUserProfileGetResponse, GetUserProfileBffUserProfileGetResponses, HealthHealthGetData, HealthHealthGetResponse, HealthHealthGetResponses, HttpValidationError, JobListResponse, JobResponse, JobSummary, LanguagePairOption, LearnableLanguageItem, LearnableLanguageRequest, LearnableLanguageResponse, LemmaResponse, ListAdventuresApiAdventuresGetData, ListAdventuresApiAdventuresGetResponse, ListAdventuresApiAdventuresGetResponses, ListArticlesBffArticlesGetData, ListArticlesBffArticlesGetResponse, ListArticlesBffArticlesGetResponses, ListEntriesApiAdventuresAdventureIdEntriesGetData, ListEntriesApiAdventuresAdventureIdEntriesGetError, ListEntriesApiAdventuresAdventureIdEntriesGetErrors, ListEntriesApiAdventuresAdventureIdEntriesGetResponse, ListEntriesApiAdventuresAdventureIdEntriesGetResponses, ListEntriesApiVocabGetData, ListEntriesApiVocabGetError, ListEntriesApiVocabGetErrors, ListEntriesApiVocabGetResponse, ListEntriesApiVocabGetResponses, ListFlashcardsApiFlashcardsGetData, ListFlashcardsApiFlashcardsGetResponse, ListFlashcardsApiFlashcardsGetResponses, ListPacksApiAdminPacksGetData, ListPacksApiAdminPacksGetError, ListPacksApiAdminPacksGetErrors, ListPacksApiAdminPacksGetResponse, ListPacksApiAdminPacksGetResponses, ListPacksApiPacksGetData, ListPacksApiPacksGetError, ListPacksApiPacksGetErrors, ListPacksApiPacksGetResponse, ListPacksApiPacksGetResponses, ListPacksForSelectionBffPacksGetData, ListPacksForSelectionBffPacksGetError, ListPacksForSelectionBffPacksGetErrors, ListPacksForSelectionBffPacksGetResponse, ListPacksForSelectionBffPacksGetResponses, LoginApiAuthLoginPostData, LoginApiAuthLoginPostError, LoginApiAuthLoginPostErrors, LoginApiAuthLoginPostResponse, LoginApiAuthLoginPostResponses, LoginRequest, MetricsMetricsGetData, MetricsMetricsGetResponses, OnboardingRequest, OnboardingResponse, PackEntryResponse, PackResponse, PackSelectionItem, PackSummaryResponse, PendingDisambiguationApiVocabPendingDisambiguationGetData, PendingDisambiguationApiVocabPendingDisambiguationGetResponse, PendingDisambiguationApiVocabPendingDisambiguationGetResponses, PosRequest, PosResponse, ProficiencyOption, PublishPackApiAdminPacksPackIdPublishPostData, PublishPackApiAdminPacksPackIdPublishPostError, PublishPackApiAdminPacksPackIdPublishPostErrors, PublishPackApiAdminPacksPackIdPublishPostResponse, PublishPackApiAdminPacksPackIdPublishPostResponses, RecordDecisionApiAdventuresAdventureIdDecisionsPostData, RecordDecisionApiAdventuresAdventureIdDecisionsPostError, RecordDecisionApiAdventuresAdventureIdDecisionsPostErrors, RecordDecisionApiAdventuresAdventureIdDecisionsPostResponse, RecordDecisionApiAdventuresAdventureIdDecisionsPostResponses, RecordEventApiFlashcardsFlashcardIdEventsPostData, RecordEventApiFlashcardsFlashcardIdEventsPostError, RecordEventApiFlashcardsFlashcardIdEventsPostErrors, RecordEventApiFlashcardsFlashcardIdEventsPostResponse, RecordEventApiFlashcardsFlashcardIdEventsPostResponses, RecordEventRequest, RegisterApiAuthRegisterPostData, RegisterApiAuthRegisterPostError, RegisterApiAuthRegisterPostErrors, RegisterApiAuthRegisterPostResponse, RegisterApiAuthRegisterPostResponses, RegisterRequest, RegisterResponse, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteData, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteError, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteErrors, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteResponse, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteResponses, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteData, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteError, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteErrors, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponse, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponses, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteData, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteError, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteErrors, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponse, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponses, ResolveSenseApiVocabEntryIdSensePatchData, ResolveSenseApiVocabEntryIdSensePatchError, ResolveSenseApiVocabEntryIdSensePatchErrors, ResolveSenseApiVocabEntryIdSensePatchResponse, ResolveSenseApiVocabEntryIdSensePatchResponses, SearchSensesApiDictionarySensesGetData, SearchSensesApiDictionarySensesGetError, SearchSensesApiDictionarySensesGetErrors, SearchSensesApiDictionarySensesGetResponse, SearchSensesApiDictionarySensesGetResponses, SearchWordformsApiDictionaryWordformsGetData, SearchWordformsApiDictionaryWordformsGetError, SearchWordformsApiDictionaryWordformsGetErrors, SearchWordformsApiDictionaryWordformsGetResponse, SearchWordformsApiDictionaryWordformsGetResponses, SearchWordformsPrefixApiDictionarySearchGetData, SearchWordformsPrefixApiDictionarySearchGetError, SearchWordformsPrefixApiDictionarySearchGetErrors, SearchWordformsPrefixApiDictionarySearchGetResponse, SearchWordformsPrefixApiDictionarySearchGetResponses, SenseCandidateResponse, SenseMatch, SenseResponse, SetSenseRequest, TokenInfo, TokenResponse, TranslateTextApiTranslateGetData, TranslateTextApiTranslateGetError, TranslateTextApiTranslateGetErrors, TranslateTextApiTranslateGetResponse, TranslateTextApiTranslateGetResponses, TranslationResponse, UpdatePackApiAdminPacksPackIdPatchData, UpdatePackApiAdminPacksPackIdPatchError, UpdatePackApiAdminPacksPackIdPatchErrors, UpdatePackApiAdminPacksPackIdPatchResponse, UpdatePackApiAdminPacksPackIdPatchResponses, UpdatePackRequest, UpsertLearnableLanguageApiLearnableLanguagesPostData, UpsertLearnableLanguageApiLearnableLanguagesPostError, UpsertLearnableLanguageApiLearnableLanguagesPostErrors, UpsertLearnableLanguageApiLearnableLanguagesPostResponse, UpsertLearnableLanguageApiLearnableLanguagesPostResponses, UserProfileResponse, ValidationError, VerifyEmailApiAuthVerifyEmailGetData, VerifyEmailApiAuthVerifyEmailGetError, VerifyEmailApiAuthVerifyEmailGetErrors, VerifyEmailApiAuthVerifyEmailGetResponse, VerifyEmailApiAuthVerifyEmailGetResponses, WordBankEntryResponse, WordformMatch } from './types.gen'; diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts index a024697..7e09a02 100644 --- a/frontend/src/client/sdk.gen.ts +++ b/frontend/src/client/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddEntryApiAdminPacksPackIdEntriesPostData, AddEntryApiAdminPacksPackIdEntriesPostErrors, AddEntryApiAdminPacksPackIdEntriesPostResponses, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostData, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostErrors, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostResponses, AddFromTokenApiVocabFromTokenPostData, AddFromTokenApiVocabFromTokenPostErrors, AddFromTokenApiVocabFromTokenPostResponses, AddLearnableLanguageApiAccountLearnableLanguagesPostData, AddLearnableLanguageApiAccountLearnableLanguagesPostErrors, AddLearnableLanguageApiAccountLearnableLanguagesPostResponses, AddPackToBankApiPacksPackIdAddToBankPostData, AddPackToBankApiPacksPackIdAddToBankPostErrors, AddPackToBankApiPacksPackIdAddToBankPostResponses, AddWordApiVocabPostData, AddWordApiVocabPostErrors, AddWordApiVocabPostResponses, AnalyzePosApiPosPostData, AnalyzePosApiPosPostErrors, AnalyzePosApiPosPostResponses, CompleteOnboardingApiAccountOnboardingPostData, CompleteOnboardingApiAccountOnboardingPostErrors, CompleteOnboardingApiAccountOnboardingPostResponses, CreateAdventureApiAdventuresPostData, CreateAdventureApiAdventuresPostErrors, CreateAdventureApiAdventuresPostResponses, CreateArticleApiArticlesPostData, CreateArticleApiArticlesPostErrors, CreateArticleApiArticlesPostResponses, CreateGenerationJobApiGeneratePostData, CreateGenerationJobApiGeneratePostErrors, CreateGenerationJobApiGeneratePostResponses, CreatePackApiAdminPacksPostData, CreatePackApiAdminPacksPostErrors, CreatePackApiAdminPacksPostResponses, DeleteAdventureApiAdventuresAdventureIdDeleteData, DeleteAdventureApiAdventuresAdventureIdDeleteErrors, DeleteAdventureApiAdventuresAdventureIdDeleteResponses, GenerateFlashcardsApiVocabEntryIdFlashcardsPostData, GenerateFlashcardsApiVocabEntryIdFlashcardsPostErrors, GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponses, GetAccountBffAccountGetData, GetAccountBffAccountGetResponses, GetAccountStatusApiAccountStatusGetData, GetAccountStatusApiAccountStatusGetResponses, GetAdventureApiAdventuresAdventureIdGetData, GetAdventureApiAdventuresAdventureIdGetErrors, GetAdventureApiAdventuresAdventureIdGetResponses, GetAdventureAudioFileMediaAdventureAudioFilenameGetData, GetAdventureAudioFileMediaAdventureAudioFilenameGetErrors, GetAdventureAudioFileMediaAdventureAudioFilenameGetResponses, GetAdventureBffAdventureAdventureIdGetData, GetAdventureBffAdventureAdventureIdGetErrors, GetAdventureBffAdventureAdventureIdGetResponses, GetArticleApiArticlesArticleIdGetData, GetArticleApiArticlesArticleIdGetErrors, GetArticleApiArticlesArticleIdGetResponses, GetArticleBffArticlesArticleIdGetData, GetArticleBffArticlesArticleIdGetErrors, GetArticleBffArticlesArticleIdGetResponses, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetData, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetErrors, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponses, GetJobApiJobsJobIdGetData, GetJobApiJobsJobIdGetErrors, GetJobApiJobsJobIdGetResponses, GetJobsApiJobsGetData, GetJobsApiJobsGetResponses, GetMediaFileMediaFilenameGetData, GetMediaFileMediaFilenameGetErrors, GetMediaFileMediaFilenameGetResponses, GetOnboardingBffAccountOnboardingGetData, GetOnboardingBffAccountOnboardingGetResponses, GetPackApiAdminPacksPackIdGetData, GetPackApiAdminPacksPackIdGetErrors, GetPackApiAdminPacksPackIdGetResponses, GetPackApiPacksPackIdGetData, GetPackApiPacksPackIdGetErrors, GetPackApiPacksPackIdGetResponses, GetUserProfileBffUserProfileGetData, GetUserProfileBffUserProfileGetResponses, HealthHealthGetData, HealthHealthGetResponses, ListAdventuresApiAdventuresGetData, ListAdventuresApiAdventuresGetResponses, ListArticlesBffArticlesGetData, ListArticlesBffArticlesGetErrors, ListArticlesBffArticlesGetResponses, ListEntriesApiAdventuresAdventureIdEntriesGetData, ListEntriesApiAdventuresAdventureIdEntriesGetErrors, ListEntriesApiAdventuresAdventureIdEntriesGetResponses, ListEntriesApiVocabGetData, ListEntriesApiVocabGetErrors, ListEntriesApiVocabGetResponses, ListFlashcardsApiFlashcardsGetData, ListFlashcardsApiFlashcardsGetResponses, ListPacksApiAdminPacksGetData, ListPacksApiAdminPacksGetErrors, ListPacksApiAdminPacksGetResponses, ListPacksApiPacksGetData, ListPacksApiPacksGetErrors, ListPacksApiPacksGetResponses, ListPacksForSelectionBffPacksGetData, ListPacksForSelectionBffPacksGetErrors, ListPacksForSelectionBffPacksGetResponses, LoginApiAuthLoginPostData, LoginApiAuthLoginPostErrors, LoginApiAuthLoginPostResponses, MetricsMetricsGetData, MetricsMetricsGetResponses, PendingDisambiguationApiVocabPendingDisambiguationGetData, PendingDisambiguationApiVocabPendingDisambiguationGetResponses, PublishPackApiAdminPacksPackIdPublishPostData, PublishPackApiAdminPacksPackIdPublishPostErrors, PublishPackApiAdminPacksPackIdPublishPostResponses, RecordDecisionApiAdventuresAdventureIdDecisionsPostData, RecordDecisionApiAdventuresAdventureIdDecisionsPostErrors, RecordDecisionApiAdventuresAdventureIdDecisionsPostResponses, RecordEventApiFlashcardsFlashcardIdEventsPostData, RecordEventApiFlashcardsFlashcardIdEventsPostErrors, RecordEventApiFlashcardsFlashcardIdEventsPostResponses, RegisterApiAuthRegisterPostData, RegisterApiAuthRegisterPostErrors, RegisterApiAuthRegisterPostResponses, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteData, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteErrors, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteResponses, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteData, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteErrors, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponses, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteData, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteErrors, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponses, ResolveSenseApiVocabEntryIdSensePatchData, ResolveSenseApiVocabEntryIdSensePatchErrors, ResolveSenseApiVocabEntryIdSensePatchResponses, SearchSensesApiDictionarySensesGetData, SearchSensesApiDictionarySensesGetErrors, SearchSensesApiDictionarySensesGetResponses, SearchWordformsApiDictionaryWordformsGetData, SearchWordformsApiDictionaryWordformsGetErrors, SearchWordformsApiDictionaryWordformsGetResponses, SearchWordformsPrefixApiDictionarySearchGetData, SearchWordformsPrefixApiDictionarySearchGetErrors, SearchWordformsPrefixApiDictionarySearchGetResponses, TranslateTextApiTranslateGetData, TranslateTextApiTranslateGetErrors, TranslateTextApiTranslateGetResponses, UpdatePackApiAdminPacksPackIdPatchData, UpdatePackApiAdminPacksPackIdPatchErrors, UpdatePackApiAdminPacksPackIdPatchResponses, UpsertLearnableLanguageApiLearnableLanguagesPostData, UpsertLearnableLanguageApiLearnableLanguagesPostErrors, UpsertLearnableLanguageApiLearnableLanguagesPostResponses, VerifyEmailApiAuthVerifyEmailGetData, VerifyEmailApiAuthVerifyEmailGetErrors, VerifyEmailApiAuthVerifyEmailGetResponses } from './types.gen'; +import type { AddEntryApiAdminPacksPackIdEntriesPostData, AddEntryApiAdminPacksPackIdEntriesPostErrors, AddEntryApiAdminPacksPackIdEntriesPostResponses, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostData, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostErrors, AddFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsPostResponses, AddFromTokenApiVocabFromTokenPostData, AddFromTokenApiVocabFromTokenPostErrors, AddFromTokenApiVocabFromTokenPostResponses, AddLearnableLanguageApiAccountLearnableLanguagesPostData, AddLearnableLanguageApiAccountLearnableLanguagesPostErrors, AddLearnableLanguageApiAccountLearnableLanguagesPostResponses, AddPackToBankApiPacksPackIdAddToBankPostData, AddPackToBankApiPacksPackIdAddToBankPostErrors, AddPackToBankApiPacksPackIdAddToBankPostResponses, AddWordApiVocabPostData, AddWordApiVocabPostErrors, AddWordApiVocabPostResponses, AnalyzePosApiPosPostData, AnalyzePosApiPosPostErrors, AnalyzePosApiPosPostResponses, CompleteOnboardingApiAccountOnboardingPostData, CompleteOnboardingApiAccountOnboardingPostErrors, CompleteOnboardingApiAccountOnboardingPostResponses, CreateAdventureApiAdventuresPostData, CreateAdventureApiAdventuresPostErrors, CreateAdventureApiAdventuresPostResponses, CreateArticleApiArticlesPostData, CreateArticleApiArticlesPostErrors, CreateArticleApiArticlesPostResponses, CreateGenerationJobApiGeneratePostData, CreateGenerationJobApiGeneratePostErrors, CreateGenerationJobApiGeneratePostResponses, CreatePackApiAdminPacksPostData, CreatePackApiAdminPacksPostErrors, CreatePackApiAdminPacksPostResponses, DeleteAdventureApiAdventuresAdventureIdDeleteData, DeleteAdventureApiAdventuresAdventureIdDeleteErrors, DeleteAdventureApiAdventuresAdventureIdDeleteResponses, GenerateFlashcardsApiVocabEntryIdFlashcardsPostData, GenerateFlashcardsApiVocabEntryIdFlashcardsPostErrors, GenerateFlashcardsApiVocabEntryIdFlashcardsPostResponses, GetAccountBffAccountGetData, GetAccountBffAccountGetResponses, GetAccountStatusApiAccountStatusGetData, GetAccountStatusApiAccountStatusGetResponses, GetAdventureApiAdventuresAdventureIdGetData, GetAdventureApiAdventuresAdventureIdGetErrors, GetAdventureApiAdventuresAdventureIdGetResponses, GetAdventureAudioFileMediaAdventureAudioFilenameGetData, GetAdventureAudioFileMediaAdventureAudioFilenameGetErrors, GetAdventureAudioFileMediaAdventureAudioFilenameGetResponses, GetAdventureBffAdventureAdventureIdGetData, GetAdventureBffAdventureAdventureIdGetErrors, GetAdventureBffAdventureAdventureIdGetResponses, GetArticleApiArticlesArticleIdGetData, GetArticleApiArticlesArticleIdGetErrors, GetArticleApiArticlesArticleIdGetResponses, GetArticleBffArticlesArticleIdGetData, GetArticleBffArticlesArticleIdGetErrors, GetArticleBffArticlesArticleIdGetResponses, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetData, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetErrors, GetEntryApiAdventuresAdventureIdEntriesEntryIdGetResponses, GetJobApiJobsJobIdGetData, GetJobApiJobsJobIdGetErrors, GetJobApiJobsJobIdGetResponses, GetJobsApiJobsGetData, GetJobsApiJobsGetResponses, GetMediaFileMediaFilenameGetData, GetMediaFileMediaFilenameGetErrors, GetMediaFileMediaFilenameGetResponses, GetOnboardingBffAccountOnboardingGetData, GetOnboardingBffAccountOnboardingGetResponses, GetPackApiAdminPacksPackIdGetData, GetPackApiAdminPacksPackIdGetErrors, GetPackApiAdminPacksPackIdGetResponses, GetPackApiPacksPackIdGetData, GetPackApiPacksPackIdGetErrors, GetPackApiPacksPackIdGetResponses, GetUserProfileBffUserProfileGetData, GetUserProfileBffUserProfileGetResponses, HealthHealthGetData, HealthHealthGetResponses, ListAdventuresApiAdventuresGetData, ListAdventuresApiAdventuresGetResponses, ListArticlesBffArticlesGetData, ListArticlesBffArticlesGetResponses, ListEntriesApiAdventuresAdventureIdEntriesGetData, ListEntriesApiAdventuresAdventureIdEntriesGetErrors, ListEntriesApiAdventuresAdventureIdEntriesGetResponses, ListEntriesApiVocabGetData, ListEntriesApiVocabGetErrors, ListEntriesApiVocabGetResponses, ListFlashcardsApiFlashcardsGetData, ListFlashcardsApiFlashcardsGetResponses, ListPacksApiAdminPacksGetData, ListPacksApiAdminPacksGetErrors, ListPacksApiAdminPacksGetResponses, ListPacksApiPacksGetData, ListPacksApiPacksGetErrors, ListPacksApiPacksGetResponses, ListPacksForSelectionBffPacksGetData, ListPacksForSelectionBffPacksGetErrors, ListPacksForSelectionBffPacksGetResponses, LoginApiAuthLoginPostData, LoginApiAuthLoginPostErrors, LoginApiAuthLoginPostResponses, MetricsMetricsGetData, MetricsMetricsGetResponses, PendingDisambiguationApiVocabPendingDisambiguationGetData, PendingDisambiguationApiVocabPendingDisambiguationGetResponses, PublishPackApiAdminPacksPackIdPublishPostData, PublishPackApiAdminPacksPackIdPublishPostErrors, PublishPackApiAdminPacksPackIdPublishPostResponses, RecordDecisionApiAdventuresAdventureIdDecisionsPostData, RecordDecisionApiAdventuresAdventureIdDecisionsPostErrors, RecordDecisionApiAdventuresAdventureIdDecisionsPostResponses, RecordEventApiFlashcardsFlashcardIdEventsPostData, RecordEventApiFlashcardsFlashcardIdEventsPostErrors, RecordEventApiFlashcardsFlashcardIdEventsPostResponses, RegisterApiAuthRegisterPostData, RegisterApiAuthRegisterPostErrors, RegisterApiAuthRegisterPostResponses, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteData, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteErrors, RemoveEntryApiAdminPacksPackIdEntriesEntryIdDeleteResponses, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteData, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteErrors, RemoveFlashcardTemplateApiAdminPacksPackIdEntriesEntryIdFlashcardsTemplateIdDeleteResponses, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteData, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteErrors, RemoveLearnableLanguageApiAccountLearnableLanguagesLanguageIdDeleteResponses, ResolveSenseApiVocabEntryIdSensePatchData, ResolveSenseApiVocabEntryIdSensePatchErrors, ResolveSenseApiVocabEntryIdSensePatchResponses, SearchSensesApiDictionarySensesGetData, SearchSensesApiDictionarySensesGetErrors, SearchSensesApiDictionarySensesGetResponses, SearchWordformsApiDictionaryWordformsGetData, SearchWordformsApiDictionaryWordformsGetErrors, SearchWordformsApiDictionaryWordformsGetResponses, SearchWordformsPrefixApiDictionarySearchGetData, SearchWordformsPrefixApiDictionarySearchGetErrors, SearchWordformsPrefixApiDictionarySearchGetResponses, TranslateTextApiTranslateGetData, TranslateTextApiTranslateGetErrors, TranslateTextApiTranslateGetResponses, UpdatePackApiAdminPacksPackIdPatchData, UpdatePackApiAdminPacksPackIdPatchErrors, UpdatePackApiAdminPacksPackIdPatchResponses, UpsertLearnableLanguageApiLearnableLanguagesPostData, UpsertLearnableLanguageApiLearnableLanguagesPostErrors, UpsertLearnableLanguageApiLearnableLanguagesPostResponses, VerifyEmailApiAuthVerifyEmailGetData, VerifyEmailApiAuthVerifyEmailGetErrors, VerifyEmailApiAuthVerifyEmailGetResponses } from './types.gen'; export type Options = Options2 & { /** @@ -542,7 +542,7 @@ export const getAdventureBffAdventureAdventureIdGet = (options?: Options) => (options?.client ?? client).get({ +export const listArticlesBffArticlesGet = (options?: Options) => (options?.client ?? client).get({ security: [{ scheme: 'bearer', type: 'http' }], url: '/bff/articles', ...options diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index 39bdc65..b92e83f 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -409,55 +409,31 @@ export type ArticleDetail = { /** * Published At */ - published_at: string; + published_at: string | null; /** - * Source Language + * Language */ - source_language: string; + language: string; /** - * Source Title + * Complexity */ - source_title: string; + complexity: string; /** - * Source Body + * Title */ - source_body: string; + title: string; /** - * Source Body Pos + * Body */ - source_body_pos: { - [key: string]: unknown; - }; + body: string; /** - * Target Language + * Audio Url */ - target_language: string; + audio_url: string | null; /** - * Target Complexities + * Body Pos */ - target_complexities: Array; - /** - * Target Title - */ - target_title: string; - /** - * Target Body - */ - target_body: string; - /** - * Target Audio Url - */ - target_audio_url: string | null; - /** - * Target Body Pos - */ - target_body_pos: { - [key: string]: unknown; - }; - /** - * Target Body Transcript - */ - target_body_transcript: { + body_pos: { [key: string]: unknown; } | null; }; @@ -874,9 +850,9 @@ export type GenerationRequest = { */ complexity_level: string; /** - * Input Texts + * Text */ - input_texts: Array; + text: string; /** * Source Language */ @@ -1755,27 +1731,19 @@ export type AppRoutersBffArticlesArticleItem = { /** * Published At */ - published_at: string; + published_at: string | null; /** - * Source Language + * Language */ - source_language: string; + language: string; /** - * Source Title + * Title */ - source_title: string; + title: string; /** - * Target Language + * Complexity */ - target_language: string; - /** - * Target Complexities - */ - target_complexities: Array; - /** - * Target Title - */ - target_title: string; + complexity: string; }; export type AddLearnableLanguageApiAccountLearnableLanguagesPostData = { @@ -3141,24 +3109,10 @@ export type GetAdventureBffAdventureAdventureIdGetResponse = GetAdventureBffAdve export type ListArticlesBffArticlesGetData = { body?: never; path?: never; - query?: { - /** - * Target Language - */ - target_language?: string; - }; + query?: never; url: '/bff/articles'; }; -export type ListArticlesBffArticlesGetErrors = { - /** - * Validation Error - */ - 422: HttpValidationError; -}; - -export type ListArticlesBffArticlesGetError = ListArticlesBffArticlesGetErrors[keyof ListArticlesBffArticlesGetErrors]; - export type ListArticlesBffArticlesGetResponses = { /** * Successful Response diff --git a/frontend/src/routes/app/articles/+page.svelte b/frontend/src/routes/app/articles/+page.svelte index 482aff6..aff1add 100644 --- a/frontend/src/routes/app/articles/+page.svelte +++ b/frontend/src/routes/app/articles/+page.svelte @@ -37,17 +37,19 @@
  • -

    {article.target_title}

    -

    {article.source_title}

    - -
    + {lang(article.language)} + + {article.complexity} + +

    {article.title}

    + + {#if article.published_at} + + {/if} +
  • {/each} diff --git a/frontend/src/routes/app/articles/[article_id]/+page.svelte b/frontend/src/routes/app/articles/[article_id]/+page.svelte index 74754cd..3f3b1a2 100644 --- a/frontend/src/routes/app/articles/[article_id]/+page.svelte +++ b/frontend/src/routes/app/articles/[article_id]/+page.svelte @@ -7,171 +7,17 @@ import TranslationPanel from './TranslationPanel.svelte'; const { data }: PageProps = $props(); - const { article } = data; + const { + article: { published_at, language, title, audio_url, body, body_pos, complexity, id } + } = data; - // ------------------------------------------------------------------------- - // Body parsing: split into paragraphs → sentences → tokens - // ------------------------------------------------------------------------- - - function extractParagraphsAndWordCount(text: PartsOfSpeechData): { - paragraphs: Paragraph[]; - totalWords: number; - } { - const paragraphs: Paragraph[] = [{ index: 0, sentences: [] }]; - let wordIdx = 0; - let sentenceIdx = 0; - - text.sentences.forEach((s) => { - const sentence: Sentence = { - idx: sentenceIdx++, - text: s.text, - startWordIdx: wordIdx, - endWordIdx: wordIdx + s.tokens.length - 1, - tokens: s.tokens.map((t) => ({ - ...t, - idx: wordIdx++ - })) as SentenceToken[] - }; - - const sentenceEndsWithNewLine = s.text.endsWith('\n'); - paragraphs[paragraphs.length - 1].sentences.push(sentence); - - if (sentenceEndsWithNewLine) { - paragraphs.push({ index: paragraphs.length, sentences: [] }); - } - }); - - return { paragraphs, totalWords: wordIdx }; - } - - const { paragraphs } = extractParagraphsAndWordCount( - article.target_body_pos as Record as PartsOfSpeechData - ); - - // Flat source-sentence list, aligned by sentence index to the target sentences. - // Used by TranslationPanel to show the source-language context for guessing. - const sourceSentences: Array<{ text: string; tokens: PartOfSpeechToken[] }> = (() => { - try { - return (article.source_body_pos as Record as PartsOfSpeechData).sentences ?? []; - } catch { - return []; - } - })(); - - // Flat sentence list for O(n) audio-time lookup - const allSentences: Array<{ idx: number; startWordIdx: number; endWordIdx: number }> = []; - for (const para of paragraphs) { - for (const s of para.sentences) { - allSentences.push({ idx: s.idx, startWordIdx: s.startWordIdx, endWordIdx: s.endWordIdx }); - } - } - - // ------------------------------------------------------------------------- - // Transcript: extract per-word timings from Deepgram response - // ------------------------------------------------------------------------- - - type WordTiming = { start: number; end: number }; - - function extractWordTimings(transcript: Transcript | null): WordTiming[] { - if (!transcript) return []; - try { - const timings: WordTiming[] = []; - for (const utterance of transcript.utterances) { - for (const word of utterance.words) { - timings.push({ start: word.start, end: word.end }); - } - } - return timings; - } catch { - return []; - } - } - - const wordTimings = extractWordTimings( - article.target_body_transcript as unknown as Transcript | null - ); - - // ------------------------------------------------------------------------- - // Reactive state - // ------------------------------------------------------------------------- - - let audioEl: HTMLAudioElement | null = $state(null); - let activeSentenceIdx = $state(-1); - let selectedTokens: SentenceToken[] = $state([]); - let selectedSentence: Sentence | null = $state(null); - - const selectedTokenIndices = $derived(new Set(selectedTokens.map((t) => t.idx))); - - // ------------------------------------------------------------------------- - // Audio: sentence highlighting - // ------------------------------------------------------------------------- - - function handleTimeUpdate() { - if (!audioEl || wordTimings.length === 0) return; - const t = audioEl.currentTime; - - // Find the word index at current playback time - let wordIdx = -1; - for (let i = 0; i < wordTimings.length; i++) { - if (wordTimings[i].start <= t && t <= wordTimings[i].end) { - wordIdx = i; - break; - } - // Between words: use the most recently started word - if (wordTimings[i].start > t) { - wordIdx = i - 1; - break; - } - } - if (wordIdx < 0) return; - - for (const s of allSentences) { - if (s.startWordIdx <= wordIdx && wordIdx <= s.endWordIdx) { - activeSentenceIdx = s.idx; - return; - } - } - } - - // ------------------------------------------------------------------------- - // Word selection: open panel with sentence context - // ------------------------------------------------------------------------- - - function handleSelection(tokens: SentenceToken[], sentence: Sentence) { - selectedTokens = tokens; - selectedSentence = sentence; - activeSentenceIdx = sentence.idx; - } - - function closePanel() { - selectedTokens = []; - selectedSentence = null; - } - - // ------------------------------------------------------------------------- - // Display helpers - // ------------------------------------------------------------------------- - - const languageNames: Record = { - en: 'English', - fr: 'French', - es: 'Spanish', - it: 'Italian', - de: 'German', - pt: 'Portuguese', - ja: 'Japanese', - zh: 'Chinese', - ko: 'Korean' - }; - - const targetLang = - languageNames[article.target_language] ?? article.target_language.toUpperCase(); - - const publishedDate = new Intl.DateTimeFormat('en-GB', { - year: 'numeric', - month: 'long', - day: 'numeric' - }).format(new Date(article.published_at)); + const publishedDate = published_at + ? new Intl.DateTimeFormat('en-GB', { + year: 'numeric', + month: 'long', + day: 'numeric' + }).format(new Date(published_at)) + : 'Unpublished'; @@ -183,56 +29,25 @@
    -

    {targetLang} · {publishedDate}

    -

    {article.target_title}

    +

    {language} · {publishedDate}

    +

    {title}

    -
    - -
    - {#if article.target_audio_url} -
    - -
    - {/if} +
    + {#if audio_url} +
    + +
    + {/if} - -
    - - + {#each body.split('\n\n') as paragraph} +

    {paragraph}

    + {/each}
    - - -
    0} - onclick={closePanel} - aria-hidden="true" ->
    -