Add spell practice, memory analytics, auth persistence, and word library UX.

Includes per-word training stats and curves, quiz session auto-save, remember-login,
paginated word list with floating page arrows, and Obsidian-style relationship graph baseline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-04 14:57:39 -07:00
parent bd7635986a
commit 68c5efe573
30 changed files with 2560 additions and 63 deletions
+95 -2
View File
@@ -1,5 +1,5 @@
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, computed_field
# Auth
@@ -11,6 +11,7 @@ class UserRegister(BaseModel):
class UserLogin(BaseModel):
username: str
password: str
remember: bool = True
class TokenResponse(BaseModel):
@@ -72,14 +73,104 @@ class WordOut(BaseModel):
wrong_count: int
consecutive_correct_count: int
mastery_score: int
train_count: int = 0
total_train_seconds: int = 0
review_due_date: Optional[str] = None
last_reviewed_at: Optional[str] = None
created_at: str
@computed_field # type: ignore[prop-decorator]
@property
def entered_at(self) -> str:
"""词库进入时间(与 created_at 一致)。"""
return self.created_at
class Config:
from_attributes = True
class MemoryCurvePoint(BaseModel):
day_index: int
date: str
forgetting: float
mastery: float
risk: float
class MemoryFutureRiskPoint(BaseModel):
day_offset: int
date: str
risk: float
class MemoryGraphNode(BaseModel):
id: str
label: str
zh: str
status: str
mastery: int
entered_at: str
size: int
class MemoryGraphLink(BaseModel):
source: str
target: str
kind: str
strength: float
class MemoryGraph(BaseModel):
nodes: list[MemoryGraphNode]
links: list[MemoryGraphLink]
class MemoryWordSummary(BaseModel):
id: int
en: str
zh: str
status: str
mastery_score: int
correct_count: int = 0
wrong_count: int = 0
train_count: int = 0
total_train_seconds: int = 0
entered_at: str
retention_now: float
risk_7d: float
class WordMemoryCurvePoint(BaseModel):
date: str
datetime: str
forgetting: float
mastery: float
risk: float
wrong_count: int
train_count: int
train_seconds: int
is_correct: Optional[bool] = None
class WordMemoryDetailResponse(BaseModel):
word_id: int
en: str
zh: str
correct_count: int
wrong_count: int
train_count: int
total_train_seconds: int
curve_points: list[WordMemoryCurvePoint]
future_risk: list[MemoryFutureRiskPoint]
class MemoryVisualizationResponse(BaseModel):
curve_points: list[MemoryCurvePoint]
future_risk: list[MemoryFutureRiskPoint]
words: list[MemoryWordSummary]
graph: MemoryGraph
# Quiz
class QuizOption(BaseModel):
label: str
@@ -90,8 +181,9 @@ class QuizQuestion(BaseModel):
word_id: int
question_type: str
prompt: str
options: list[QuizOption]
options: list[QuizOption] = []
correct_answer: str
phonetic: Optional[str] = None
class DailyQuizResponse(BaseModel):
@@ -104,6 +196,7 @@ class QuizAnswerRequest(BaseModel):
question_type: str
user_answer: str
correct_answer: str
duration_seconds: Optional[int] = Field(None, ge=0, le=3600)
class QuizAnswerResponse(BaseModel):