Ship native iOS app, Wiki/TTS backend, and skeleton loading UX.

Replace the WebView shell with SwiftUI screens, add account-scoped Wiki and TTS APIs with adaptive review and photo scan support, and keep web/iOS pages usable while data loads asynchronously.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-08 15:13:59 +08:00
parent db5465c646
commit 05e173b293
115 changed files with 12709 additions and 1212 deletions
+139
View File
@@ -61,6 +61,25 @@ class WordCreate(BaseModel):
example_cn: Optional[str] = None
class WordScanTextRequest(BaseModel):
text: str = Field(min_length=1)
class WordScanItem(BaseModel):
source_text: str
target_text: str
source_lang: str = "en"
target_lang: str = "zh"
class WordScanResponse(BaseModel):
items: list[WordScanItem]
class WordBatchCreateRequest(BaseModel):
items: list[WordCreate] = Field(min_length=1, max_length=200)
class WordUpdate(BaseModel):
status: Optional[str] = None
@@ -83,7 +102,13 @@ class WordOut(BaseModel):
consecutive_correct_count: int
mastery_score: int
train_count: int = 0
error_bank_member: int = 0
error_reinforce_streak: int = 0
error_bank_entered_at: Optional[str] = None
total_train_seconds: int = 0
difficulty: float = 5.0
stability_hours: float = 24.0
next_review_at: Optional[str] = None
review_due_date: Optional[str] = None
last_reviewed_at: Optional[str] = None
created_at: str
@@ -98,6 +123,19 @@ class WordOut(BaseModel):
from_attributes = True
class WordListPageOut(BaseModel):
items: list[WordOut]
total: int
page: int
page_size: int
class WordBatchCreateResponse(BaseModel):
created: int
skipped: int
items: list[WordOut]
class MemoryCurvePoint(BaseModel):
day_index: int
date: str
@@ -193,6 +231,11 @@ class QuizQuestion(BaseModel):
options: list[QuizOption] = []
correct_answer: str
phonetic: Optional[str] = None
train_count: int = 0
wrong_count: int = 0
error_reinforce_streak: int = 0
error_clear_correct_count: int = 2
error_bank_member: int = 0
class DailyQuizResponse(BaseModel):
@@ -200,10 +243,12 @@ class DailyQuizResponse(BaseModel):
total: int
track: str = "accumulation"
book_id: Optional[int] = None
pool: str = "untrained"
class QuizAnswerRequest(BaseModel):
word_id: int
pool: Optional[str] = None
question_type: str
user_answer: str
correct_answer: str
@@ -228,6 +273,36 @@ class QuizStatsResponse(BaseModel):
daily_target: int
today_completed: int
streak_days: int
untrained_pending: int = 0
error_pending: int = 0
error_bank_due_pending: int = 0
error_bank_total: int = 0
track: str = "accumulation"
book_id: Optional[int] = None
class PracticePlanResetResponse(BaseModel):
reset_count: int
track: str = "accumulation"
book_id: Optional[int] = None
class ErrorBankWordOut(BaseModel):
word_id: int
en: str
zh: str
wrong_count: int
train_count: int
error_reinforce_streak: int
status: str
next_review_at: Optional[str] = None
review_due_date: Optional[str] = None
class ErrorBankListResponse(BaseModel):
items: list[ErrorBankWordOut]
total: int
due_count: int
track: str = "accumulation"
book_id: Optional[int] = None
@@ -275,6 +350,7 @@ class BookPracticeSettingsOut(BaseModel):
daily_target: int
master_required_count: int
weak_wrong_threshold: int
error_clear_correct_count: int
class ActiveBookOut(BaseModel):
@@ -292,6 +368,7 @@ class BookPracticeSettingsUpdate(BaseModel):
daily_target: Optional[int] = Field(None, ge=5, le=50)
master_required_count: Optional[int] = Field(None, ge=1, le=10)
weak_wrong_threshold: Optional[int] = Field(None, ge=1, le=10)
error_clear_correct_count: Optional[int] = Field(None, ge=1, le=5)
# Settings
@@ -299,6 +376,7 @@ class SettingsOut(BaseModel):
daily_target: int
master_required_count: int
weak_wrong_threshold: int
error_clear_correct_count: int
class Config:
from_attributes = True
@@ -308,6 +386,62 @@ class SettingsUpdate(BaseModel):
daily_target: Optional[int] = Field(None, ge=1, le=100)
master_required_count: Optional[int] = Field(None, ge=1, le=20)
weak_wrong_threshold: Optional[int] = Field(None, ge=1, le=20)
error_clear_correct_count: Optional[int] = Field(None, ge=1, le=5)
# Wiki
class WikiPagePreview(BaseModel):
slug: str
title: str
category: str
summary: str
content: str
source_count: int
updated_at: str
class WikiStatusOut(BaseModel):
enabled: bool
auto_organize: bool
page_count: int
link_count: int
last_organized_at: Optional[str] = None
latest_page: Optional[WikiPagePreview] = None
llm_configured: bool = False
llm_model: Optional[str] = None
compiler_mode: str = "deterministic"
daily_llm_limit: int = 20
llm_used_today: int = 0
llm_remaining_today: int = 20
quota_exceeded: bool = False
recharge_message: Optional[str] = None
class WikiGraphNodeOut(BaseModel):
id: str
slug: str
title: str
category: str
summary: str
source_count: int
updated_at: str
degree: int = 0
class WikiGraphLinkOut(BaseModel):
source: str
target: str
relation: str
class WikiGraphOut(BaseModel):
nodes: list[WikiGraphNodeOut]
links: list[WikiGraphLinkOut]
class WikiSettingsUpdate(BaseModel):
enabled: Optional[bool] = None
auto_organize: Optional[bool] = None
# Memory coach (Q/K/V dialogue)
@@ -326,6 +460,8 @@ class CoachWordBrief(BaseModel):
retention_now: float
mastery_score: int
status: str
retrievability: float = 0.0
next_review_at: Optional[str] = None
class CoachSessionResponse(BaseModel):
@@ -382,3 +518,6 @@ class CoachTurnResponse(BaseModel):
hints_used: int = 0
target_en: Optional[str] = None
target_zh: Optional[str] = None
review_message: Optional[str] = None
retrievability: Optional[float] = None
next_review_at: Optional[str] = None