feat(cefr): CEFR level testing and automated gap ingestion engine (Task 15)

This commit is contained in:
2026-10-06 15:24:33 +03:00
parent 1c3f03a64c
commit 3fae321322
11 changed files with 2926 additions and 10 deletions
+87
View File
@@ -31,6 +31,12 @@ from app.srs import calculate_sm2, generate_quiz_options, build_activity_heatmap
from app.reader import parse_epub, parse_text, parse_pdf, count_words
from app.ai import translate_sentence_ai, get_ollama_status
from app.phrasal import detect_phrasal_verb
from app.cefr import (
get_cefr_level_summaries,
get_cefr_quiz_batch,
process_cefr_answer,
reset_user_cefr_level,
)
@@ -162,6 +168,16 @@ class SentenceTranslateRequest(BaseModel):
target_word: Optional[str] = ""
class CefrAnswerRequest(BaseModel):
question_id: int
selected_option: str
class CefrResetRequest(BaseModel):
level: str
# ---------------------------------------------------------------------------
# PWA Static Endpoints
@@ -543,6 +559,18 @@ async def api_create_card(card: CardCreateRequest, user: dict = Depends(require_
@app.get("/api/cards/{card_id}")
async def api_get_card(card_id: int, user: dict = Depends(require_user)):
with get_db() as conn:
row = conn.execute(
"SELECT * FROM vocabulary_cards WHERE id = ? AND user_id = ?",
(card_id, user["id"]),
).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Card not found")
return dict(row)
@app.put("/api/cards/{card_id}")
async def api_update_card(card_id: int, update: CardUpdateRequest, user: dict = Depends(require_user)):
with get_db() as conn:
@@ -1352,3 +1380,62 @@ async def api_delete_book(book_id: int, user: dict = Depends(require_user)):
if cursor.rowcount == 0:
raise HTTPException(status_code=404, detail="Book not found")
return {"status": "ok", "deleted_id": book_id}
# ---------------------------------------------------------------------------
# CEFR Level Testing & Automated Gap Ingestion Endpoints (Task 15)
# ---------------------------------------------------------------------------
@app.get("/api/cefr/levels")
async def api_cefr_levels(user: dict = Depends(require_user)):
"""
Returns progress summaries for all CEFR levels (A1, A2, B1, B2).
"""
with get_db() as conn:
summaries = get_cefr_level_summaries(conn, user["id"])
return {"levels": summaries}
@app.get("/api/cefr/quiz")
async def api_cefr_quiz(
level: str = Query("A1"),
count: int = Query(10, ge=1, le=30),
mode: str = Query("all"),
user: dict = Depends(require_user),
):
"""
Returns next batch of Cloze Tap test questions for level (A1..B2).
"""
with get_db() as conn:
quiz_items = get_cefr_quiz_batch(conn, user["id"], level, count, mode)
return {"level": level.upper(), "count": len(quiz_items), "quiz": quiz_items}
@app.post("/api/cefr/answer")
async def api_cefr_answer(
req: CefrAnswerRequest,
user: dict = Depends(require_user),
):
"""
Submits an answer for a CEFR question.
If wrong, automatically ingests into user's SRS active vocabulary with status 'learning'.
"""
with get_db() as conn:
result = process_cefr_answer(conn, user["id"], req.question_id, req.selected_option)
if "error" in result:
raise HTTPException(status_code=404, detail=result["error"])
return result
@app.post("/api/cefr/reset")
async def api_cefr_reset(
req: CefrResetRequest,
user: dict = Depends(require_user),
):
"""
Resets user testing progress for a given CEFR level.
"""
with get_db() as conn:
deleted = reset_user_cefr_level(conn, user["id"], req.level)
return {"status": "ok", "level": req.level.upper(), "reset_count": deleted}