Add persian-tutor: Gradio-based GCSE Persian language learning app

Vocabulary study with FSRS spaced repetition, AI tutoring (Ollama/Claude),
essay marking, idioms browser, Anki export, and dashboard. 918 vocabulary
entries across 39 categories. 41 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
local
2026-02-08 01:57:44 +00:00
parent 104da381fb
commit 2e8c2c11d0
22 changed files with 10664 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
"""Dashboard: progress stats, charts, and overview."""
import db
from modules.vocab import load_vocab, get_categories
def get_overview():
"""Return overview stats: total words, seen, mastered, due today."""
vocab = load_vocab()
counts = db.get_word_counts(total_vocab_size=len(vocab))
stats = db.get_stats()
counts["streak"] = stats["streak"]
counts["total_reviews"] = stats["total_reviews"]
counts["total_quizzes"] = stats["total_quizzes"]
return counts
def get_category_breakdown():
"""Return progress per category as list of dicts."""
vocab = load_vocab()
categories = get_categories()
breakdown = []
for cat in categories:
cat_words = [e for e in vocab if e["category"] == cat]
cat_ids = {e["id"] for e in cat_words}
total = len(cat_words)
seen = 0
mastered = 0
for wid in cat_ids:
progress = db.get_word_progress(wid)
if progress:
seen += 1
if progress["stability"] and progress["stability"] > 10:
mastered += 1
breakdown.append({
"Category": cat,
"Total": total,
"Seen": seen,
"Mastered": mastered,
"Progress": f"{seen}/{total}" if total > 0 else "0/0",
})
return breakdown
def get_recent_quizzes(limit=10):
"""Return recent quiz results as list of dicts for display."""
stats = db.get_stats()
quizzes = stats["recent_quizzes"][:limit]
result = []
for q in quizzes:
result.append({
"Date": q["timestamp"],
"Category": q["category"] or "All",
"Score": f"{q['correct']}/{q['total_questions']}",
"Duration": f"{q['duration_seconds'] or 0}s",
})
return result
def format_overview_markdown():
"""Format overview stats as a markdown string for display."""
o = get_overview()
pct = (o["seen"] / o["total"] * 100) if o["total"] > 0 else 0
bar_filled = int(pct / 5)
bar_empty = 20 - bar_filled
progress_bar = "" * bar_filled + "" * bar_empty
lines = [
"## Dashboard",
"",
f"**Words studied:** {o['seen']} / {o['total']} ({pct:.0f}%)",
f"`{progress_bar}`",
"",
f"**Due today:** {o['due']}",
f"**Mastered:** {o['mastered']}",
f"**Daily streak:** {o['streak']} day{'s' if o['streak'] != 1 else ''}",
f"**Total reviews:** {o['total_reviews']}",
f"**Quiz sessions:** {o['total_quizzes']}",
]
return "\n".join(lines)