Files
breakpilot-lehrer/backend-lehrer/progress_api.py
Benjamin Admin 9dddd80d7a
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 45s
CI / test-python-agent-core (push) Has been cancelled
CI / test-nodejs-website (push) Has been cancelled
CI / test-python-klausur (push) Has started running
Add Phases 3.2-4.3: STT, stories, syllables, gamification
Phase 3.2 — MicrophoneInput.tsx: Browser Web Speech API for
speech-to-text recognition (EN+DE), integrated for pronunciation practice.

Phase 4.1 — Story Generator: LLM-powered mini-stories using vocabulary
words, with highlighted vocab in HTML output. Backend endpoint
POST /learning-units/{id}/generate-story + frontend /learn/[unitId]/story.

Phase 4.2 — SyllableBow.tsx: SVG arc component for syllable visualization
under words, clickable for per-syllable TTS.

Phase 4.3 — Gamification system:
- CoinAnimation.tsx: Floating coin rewards with accumulator
- CrownBadge.tsx: Crown/medal display for milestones
- ProgressRing.tsx: Circular progress indicator
- progress_api.py: Backend tracking coins, crowns, streaks per unit

Also adds "Geschichte" exercise type button to UnitCard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:22:52 +02:00

132 lines
3.7 KiB
Python

"""
Progress API — Tracks student learning progress per unit.
Stores coins, crowns, streak data, and exercise completion stats.
Uses JSON file storage (same pattern as learning_units.py).
"""
import os
import json
import logging
from datetime import datetime, date
from typing import Dict, Any, Optional, List
from pathlib import Path
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/progress",
tags=["progress"],
)
PROGRESS_DIR = os.path.expanduser("~/Arbeitsblaetter/Lerneinheiten/progress")
def _ensure_dir():
os.makedirs(PROGRESS_DIR, exist_ok=True)
def _progress_path(unit_id: str) -> Path:
return Path(PROGRESS_DIR) / f"{unit_id}.json"
def _load_progress(unit_id: str) -> Dict[str, Any]:
path = _progress_path(unit_id)
if path.exists():
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
return {
"unit_id": unit_id,
"coins": 0,
"crowns": 0,
"streak_days": 0,
"last_activity": None,
"exercises": {
"flashcards": {"completed": 0, "correct": 0, "incorrect": 0},
"quiz": {"completed": 0, "correct": 0, "incorrect": 0},
"type": {"completed": 0, "correct": 0, "incorrect": 0},
"story": {"generated": 0},
},
"created_at": datetime.now().isoformat(),
}
def _save_progress(unit_id: str, data: Dict[str, Any]):
_ensure_dir()
path = _progress_path(unit_id)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
class RewardPayload(BaseModel):
exercise_type: str # flashcards, quiz, type, story
correct: bool = True
first_try: bool = True
@router.get("/{unit_id}")
def get_progress(unit_id: str):
"""Get learning progress for a unit."""
return _load_progress(unit_id)
@router.post("/{unit_id}/reward")
def add_reward(unit_id: str, payload: RewardPayload):
"""Record an exercise result and award coins."""
progress = _load_progress(unit_id)
# Update exercise stats
ex = progress["exercises"].get(payload.exercise_type, {"completed": 0, "correct": 0, "incorrect": 0})
ex["completed"] = ex.get("completed", 0) + 1
if payload.correct:
ex["correct"] = ex.get("correct", 0) + 1
else:
ex["incorrect"] = ex.get("incorrect", 0) + 1
progress["exercises"][payload.exercise_type] = ex
# Award coins
if payload.correct:
coins = 3 if payload.first_try else 1
else:
coins = 0
progress["coins"] = progress.get("coins", 0) + coins
# Update streak
today = date.today().isoformat()
last = progress.get("last_activity")
if last != today:
if last == (date.today().replace(day=date.today().day - 1)).isoformat() if date.today().day > 1 else None:
progress["streak_days"] = progress.get("streak_days", 0) + 1
elif last != today:
progress["streak_days"] = 1
progress["last_activity"] = today
# Award crowns for milestones
total_correct = sum(
e.get("correct", 0) for e in progress["exercises"].values() if isinstance(e, dict)
)
progress["crowns"] = total_correct // 20 # 1 crown per 20 correct answers
_save_progress(unit_id, progress)
return {
"coins_awarded": coins,
"total_coins": progress["coins"],
"crowns": progress["crowns"],
"streak_days": progress["streak_days"],
}
@router.get("/")
def list_all_progress():
"""List progress for all units."""
_ensure_dir()
results = []
for f in Path(PROGRESS_DIR).glob("*.json"):
with open(f, "r", encoding="utf-8") as fh:
results.append(json.load(fh))
return results