| """Export study artifacts: final sheet, Anki CSV.""" |
|
|
| from __future__ import annotations |
|
|
| import csv |
| import io |
| from typing import Any |
|
|
| from plane_mode_scholar.core.config import DEFAULT_USER_ID |
| from plane_mode_scholar.memory.scheduler import get_due_reviews |
| from plane_mode_scholar.storage.sqlite_store import SQLiteStore |
| from plane_mode_scholar.study.mastery import MasteryTracker |
|
|
|
|
| def build_final_sheet_markdown( |
| store: SQLiteStore, |
| pack_id: str, |
| user_id: str = DEFAULT_USER_ID, |
| ) -> str: |
| pack = store.get_pack(pack_id) |
| mastery = MasteryTracker(store) |
| due = get_due_reviews(store, pack_id, user_id) |
| weak = mastery.get_weak_topics(pack_id) |
| topics = store.list_topics(pack_id) |
| misconceptions = [ |
| m for m in store.list_memories(pack_id=pack_id, user_id=user_id, status="active") |
| if m.type.value == "misconception" |
| ] |
|
|
| lines = [ |
| f"# Final sheet — {pack.name if pack else pack_id}", |
| "", |
| "## Must-review (due now)", |
| ] |
| if due: |
| for m in due[:8]: |
| lines.append(f"- {m.content}") |
| else: |
| lines.append("- None due — great job!") |
|
|
| lines.append("") |
| lines.append("## Weak topics") |
| if weak: |
| for w in weak[:6]: |
| lines.append(f"- **{w['name']}** ({int(w.get('mastery', 0) * 100)}%)") |
| else: |
| lines.append("- Complete quizzes to identify weak areas.") |
|
|
| lines.append("") |
| lines.append("## Common gaps") |
| if misconceptions: |
| for m in misconceptions[:6]: |
| lines.append(f"- {m.content[:120]}") |
| else: |
| lines.append("- No recorded misconceptions yet.") |
|
|
| lines.append("") |
| lines.append("## Syllabus checklist") |
| for t in topics[:12]: |
| lines.append(f"- [ ] {t.name}") |
|
|
| lines.append("") |
| lines.append("---") |
| lines.append("*Generated by Plane Mode Scholar — study offline, remember on landing.*") |
| return "\n".join(lines) |
|
|
|
|
| def build_anki_csv( |
| store: SQLiteStore, |
| pack_id: str, |
| user_id: str = DEFAULT_USER_ID, |
| ) -> str: |
| """Anki-compatible CSV: front, back, tags.""" |
| buf = io.StringIO() |
| writer = csv.writer(buf, quoting=csv.QUOTE_MINIMAL) |
| writer.writerow(["Front", "Back", "Tags"]) |
|
|
| memories = store.list_memories(pack_id=pack_id, user_id=user_id, status="active") |
| for mem in memories: |
| if mem.type.value in ("misconception", "review_item", "open_loop"): |
| front = mem.content[:200] |
| if mem.type.value == "misconception" and " instead of " in mem.content: |
| parts = mem.content.split(" instead of ") |
| back = parts[-1].strip("'") if parts else mem.content |
| else: |
| back = mem.content |
| tags = " ".join(t for t in mem.tags[:5] if not t.startswith(("due:", "interval:", "reps:"))) |
| writer.writerow([front, back[:300], tags or mem.type.value]) |
|
|
| chunks = store.list_chunks(pack_id)[:20] |
| for chunk in chunks: |
| text = chunk.text.strip() |
| if len(text) < 40: |
| continue |
| sentences = [s.strip() for s in text.split(".") if len(s.strip()) > 25] |
| if sentences: |
| front = f"What does {chunk.source_file} say about this topic?" |
| back = sentences[0][:300] |
| writer.writerow([front, back, f"chunk {chunk.source_file}"]) |
|
|
| return buf.getvalue() |
|
|
|
|
| def build_final_sheet_html( |
| store: SQLiteStore, |
| pack_id: str, |
| user_id: str = DEFAULT_USER_ID, |
| ) -> str: |
| md = build_final_sheet_markdown(store, pack_id, user_id) |
| body = md.replace("\n## ", "\n<h2>").replace("\n- ", "\n<li>") |
| body = body.replace("# ", "<h1>").replace("**", "<strong>", 1) |
| return f'<div class="pms-card pms-final-sheet"><pre class="pms-sheet">{md}</pre></div>' |
|
|