Spaces:
Sleeping
Sleeping
| """ | |
| firebase_progress.py | |
| -------------------- | |
| 學生學習進度的讀寫邏輯。 | |
| 放在專案根目錄,由 dt_callbacks.py 和頁面呼叫。 | |
| Firestore 資料結構: | |
| students/{student_id}/ | |
| profile: { name, student_id, last_seen } | |
| progress: { quiz_correct, solutions_viewed, ai_hints_used, ... } | |
| events: [ { type, unit, qid, ts, ... }, ... ] ← 子集合 | |
| """ | |
| from __future__ import annotations | |
| import datetime | |
| from firebase_init import get_db | |
| # ─── 時間戳 ──────────────────────────────────────────────────────────────── | |
| def _now() -> str: | |
| return datetime.datetime.now(datetime.timezone.utc).isoformat() | |
| # ─── 學生個人資料 ────────────────────────────────────────────────────────── | |
| def upsert_student(student_id: str, name: str = "") -> None: | |
| """建立或更新學生個人資料。""" | |
| db = get_db() | |
| ref = db.collection("students").document(student_id) | |
| ref.set({ | |
| "student_id": student_id, | |
| "name": name or student_id, | |
| "last_seen": _now(), | |
| }, merge=True) | |
| # ─── 記錄事件 ───────────────────────────────────────────────────────────── | |
| def log_event(student_id: str, event_type: str, **kwargs) -> None: | |
| """ | |
| 記錄一筆學習事件到 students/{student_id}/events 子集合。 | |
| event_type 範例: | |
| "quiz_correct" - 填空/選擇題答對 | |
| "quiz_wrong" - 答錯 | |
| "solution_viewed" - 展開詳細解法 | |
| "hint_used" - 按下 AI 提示 | |
| "quiz_ai_done" - AI 批改完成 | |
| """ | |
| db = get_db() | |
| payload = { | |
| "type": event_type, | |
| "ts": _now(), | |
| **kwargs, # unit, qid, answer, ... | |
| } | |
| db.collection("students").document(student_id)\ | |
| .collection("events").add(payload) | |
| def log_quiz_result( | |
| student_id: str, | |
| unit: str, | |
| qid: str, | |
| correct: bool, | |
| answer: str = "", | |
| ) -> None: | |
| """記錄一題答題結果,同時更新 progress 計數。""" | |
| event_type = "quiz_correct" if correct else "quiz_wrong" | |
| log_event(student_id, event_type, unit=unit, qid=qid, answer=answer) | |
| # 更新統計計數 | |
| db = get_db() | |
| ref = db.collection("students").document(student_id) | |
| field = "quiz_correct_count" if correct else "quiz_wrong_count" | |
| ref.set({field: _increment(1), "last_seen": _now()}, merge=True) | |
| def log_solution_viewed(student_id: str, unit: str, solution_id: str) -> None: | |
| """記錄學生展開了某個解法區塊。""" | |
| log_event(student_id, "solution_viewed", unit=unit, solution_id=solution_id) | |
| db = get_db() | |
| db.collection("students").document(student_id)\ | |
| .set({"solutions_viewed_count": _increment(1), "last_seen": _now()}, merge=True) | |
| def log_choice_option(unit: str, qid: str, option_key: str) -> None: | |
| """記錄某題某選項被選擇的次數(全班統計,存在 question_stats 集合)。""" | |
| db = get_db() | |
| ref = ( | |
| db.collection("question_stats") | |
| .document(f"{unit}__{qid}") | |
| ) | |
| ref.set({ | |
| "unit": unit, | |
| "qid": qid, | |
| f"option_{option_key}": _increment(1), | |
| }, merge=True) | |
| def log_quiz_question_stat(unit: str, qid: str, correct: bool) -> None: | |
| """記錄某題全班答對/答錯次數(存在 question_stats 集合)。""" | |
| db = get_db() | |
| ref = db.collection("question_stats").document(f"{unit}__{qid}") | |
| field = "correct_count" if correct else "wrong_count" | |
| ref.set({"unit": unit, "qid": qid, field: _increment(1)}, merge=True) | |
| def get_all_question_stats() -> list[dict]: | |
| """讀取所有題目的答題統計(老師後台用)。""" | |
| db = get_db() | |
| docs = db.collection("question_stats").stream() | |
| return [d.to_dict() for d in docs] | |
| def register_question( | |
| unit: str, | |
| qid: str, | |
| qtype: str, | |
| prompt_runs: list | None = None, | |
| prompt_md: str = "", | |
| options: list | None = None, | |
| correct_answer: str | list | None = None, | |
| ) -> None: | |
| """ | |
| 在學生作答時,記錄題目原始內容到 question_stats。 | |
| 每次都更新(允許覆蓋),確保每道題目各自正確記錄。 | |
| prompt_runs: 題目的 run 清單(含文字與 LaTeX) | |
| prompt_md: 題目的 markdown 純文字(備援) | |
| options: 選項清單 [{"key": "a", "text": "..."}, ...] | |
| correct_answer: 正確答案 key(單選為 str,多選為 list) | |
| """ | |
| db = get_db() | |
| ref = db.collection("question_stats").document(f"{unit}__{qid}") | |
| update: dict = {"unit": unit, "qid": qid, "qtype": qtype} | |
| if prompt_runs is not None: | |
| # 只存 text/latex 欄位,避免多餘 key 佔空間 | |
| update["prompt_runs"] = [ | |
| {k: v for k, v in r.items() if k in ("text", "latex", "kind", "style", "newline")} | |
| for r in (prompt_runs or []) | |
| ] | |
| if prompt_md: | |
| update["prompt_md"] = prompt_md[:300] | |
| if options: | |
| update["options"] = [{"key": o.get("key", ""), "text": o.get("text", "")} for o in options] | |
| if correct_answer is not None: | |
| if isinstance(correct_answer, list): | |
| update["correct_answers"] = [str(k).lower() for k in correct_answer] | |
| else: | |
| update["correct_answers"] = [str(correct_answer).lower()] | |
| ref.set(update, merge=True) | |
| def log_hint_used(student_id: str, unit: str, qid: str) -> None: | |
| """記錄學生使用了 AI 提示。""" | |
| log_event(student_id, "hint_used", unit=unit, qid=qid) | |
| db = get_db() | |
| db.collection("students").document(student_id)\ | |
| .set({"ai_hints_used_count": _increment(1), "last_seen": _now()}, merge=True) | |
| # ─── 讀取 ──────────────────────────────────────────────────────────────── | |
| def get_student_progress(student_id: str) -> dict: | |
| """讀取單一學生的 progress 計數(老師後台用)。""" | |
| db = get_db() | |
| doc = db.collection("students").document(student_id).get() | |
| if doc.exists: | |
| return doc.to_dict() | |
| return {} | |
| def get_all_students() -> list[dict]: | |
| """讀取所有學生的 profile(老師後台列表用)。""" | |
| db = get_db() | |
| docs = db.collection("students").stream() | |
| return [d.to_dict() for d in docs] | |
| def get_student_events(student_id: str, limit: int = 100) -> list[dict]: | |
| """讀取某學生最近的事件記錄。""" | |
| db = get_db() | |
| docs = ( | |
| db.collection("students").document(student_id) | |
| .collection("events") | |
| .order_by("ts", direction="DESCENDING") | |
| .limit(limit) | |
| .stream() | |
| ) | |
| return [d.to_dict() for d in docs] | |
| # ─── Firestore 遞增輔助 ─────────────────────────────────────────────────── | |
| def _increment(n: int): | |
| from google.cloud.firestore import Increment | |
| return Increment(n) | |