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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user