""" 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)