Spaces:
Running on Zero
Running on Zero
| """ | |
| Recall — Gradio app. OWNER: Arturo (Module C) | |
| Three views (Upload / Study / Recap) over a single Session held in gr.State. | |
| Wired to content_pipeline + learning_engine. Runs against stubs immediately: | |
| pip install -r requirements.txt | |
| python app.py # opens at http://127.0.0.1:7860 in stub mode | |
| Flip RECALL_STUB=0 once the real model is wired: | |
| RECALL_STUB=0 python app.py | |
| """ | |
| from __future__ import annotations | |
| import gradio as gr | |
| import content_pipeline as cp | |
| import learning_engine as le | |
| # ---- Actions (UI <-> modules glue) ----------------------------------------- | |
| def start_session(file, pasted_text): | |
| if pasted_text and pasted_text.strip(): | |
| text = pasted_text.strip() | |
| else: | |
| try: | |
| text = cp.extract_text(file) | |
| except cp.ExtractionError as e: | |
| return (None, gr.update(visible=True), gr.update(visible=False), | |
| f"⚠️ {e}", "", "", gr.update(visible=False), None) | |
| if not text: | |
| return (None, gr.update(visible=True), gr.update(visible=False), | |
| "⚠️ No text found. Upload a PDF or paste some notes.", "", "", | |
| gr.update(visible=False), None) | |
| chunks = cp.chunk_text(text) | |
| debug = { | |
| "total_chars": len(text), | |
| "extracted_text_preview": text[:800] + ("…" if len(text) > 800 else ""), | |
| "chunk_count": len(chunks), | |
| "chunks": [ | |
| { | |
| "index": i, | |
| "chars": len(c), | |
| "tokens_approx": len(c) // 4, | |
| "text": c, | |
| } | |
| for i, c in enumerate(chunks) | |
| ], | |
| } | |
| try: | |
| deck = cp.generate_deck(text) | |
| except Exception as e: | |
| return (None, gr.update(visible=True), gr.update(visible=False), | |
| f"⚠️ generate_deck failed ({type(e).__name__}: {e}). " | |
| "Extraction & chunks are shown below.", | |
| "", "", gr.update(visible=True), debug) | |
| if not deck: | |
| return (None, gr.update(visible=True), gr.update(visible=False), | |
| "⚠️ Couldn't generate questions from that. Try different material.", | |
| "", "", gr.update(visible=True), debug) | |
| session = le.init_session(deck) | |
| card = le.next_card(session) | |
| return (session, gr.update(visible=False), gr.update(visible=True), | |
| "", _progress(session), card["question"] if card else "", | |
| gr.update(visible=True), debug) | |
| def submit_answer(session, user_answer): | |
| if not session: | |
| return session, "", "", "", gr.update() | |
| grade, fups = le.grade_and_adapt(session, user_answer or "") | |
| if grade is None: | |
| return session, "🎉 Deck complete!", "", "", gr.update(visible=False) | |
| followup_note = "" | |
| if fups: | |
| followup_note = ("🎯 Added " + str(len(fups)) + | |
| " new question(s) targeting what you missed.") | |
| verdict = ("✅ " if grade["correct"] else "❌ ") + grade["explanation"] | |
| return session, verdict, followup_note, _progress(session), gr.update() | |
| def change_difficulty(session, direction): | |
| """Difficulty dial (NAH-32): rewrite the current card harder/easier on the | |
| same concept and re-show it. No-op if there's no current card.""" | |
| if not session: | |
| return session, "", "" | |
| card = le.next_card(session) | |
| if card is None: | |
| return session, "", "" | |
| new = cp.regenerate(card, direction) | |
| session = le.replace_card(session, card["id"], new) | |
| label = "harder" if direction == "harder" else "easier" | |
| return session, new["question"], f"🎚️ Rewrote this question to be {label}." | |
| def show_next(session): | |
| card = le.next_card(session) | |
| if card is None: | |
| return session, "", gr.update(visible=False), gr.update(visible=True), _render_recap(session) | |
| return session, card["question"], gr.update(), gr.update(visible=False), "" | |
| def finish_session(session): | |
| """End the session on demand and show the recap (NAH-35). The spaced- | |
| repetition queue never empties on its own, so this is the user's way out.""" | |
| if not session: | |
| return gr.update(), gr.update(), "" | |
| return gr.update(visible=False), gr.update(visible=True), _render_recap(session) | |
| def resume_study(session): | |
| """Return from the recap to keep studying (so recap isn't a dead end).""" | |
| card = le.next_card(session) | |
| return (gr.update(visible=True), gr.update(visible=False), | |
| card["question"] if card else "") | |
| def _progress(session): | |
| total = len(session["deck"]) | |
| answered = len(session["history"]) | |
| current = min(answered + 1, total) | |
| remaining = len(session["queue"]) | |
| return (f"**Card {current} of {total}** · ✅ {answered} answered" | |
| f" · 🔥 Streak: {session['streak']} · {remaining} left in queue") | |
| def _render_recap(session): | |
| r = le.recap(session) | |
| return ( | |
| f"### Session recap\n\n" | |
| f"**Answered:** {r['answered']} · **🔥 Streak:** {r['streak']}\n\n" | |
| f"**Mastered:** {', '.join(r['mastered']) or '—'}\n\n" | |
| f"**Still weak:** {', '.join(r['weak_topics']) or '—'}\n\n" | |
| f"_{r['reflection']}_" | |
| ) | |
| # ---- UI --------------------------------------------------------------------- | |
| with gr.Blocks(title="Recall — your AI study partner") as demo: | |
| session = gr.State(None) | |
| gr.Markdown("# 📚 Recall\n*Upload your material. Get quizzed. It adapts to what you miss.*") | |
| # Upload view | |
| with gr.Group(visible=True) as upload_view: | |
| gr.Markdown("### 1 · Add your study material") | |
| file_in = gr.File(label="Upload a PDF or .txt", file_types=[".pdf", ".txt"]) | |
| text_in = gr.Textbox(label="…or paste notes", lines=4, | |
| placeholder="Paste any text to study from") | |
| start_btn = gr.Button("Generate deck & start", variant="primary") | |
| upload_msg = gr.Markdown("") | |
| # Study view | |
| with gr.Group(visible=False) as study_view: | |
| progress = gr.Markdown("") | |
| question = gr.Markdown("") | |
| answer_in = gr.Textbox(label="Your answer", lines=2) | |
| with gr.Row(): | |
| easier_btn = gr.Button("😌 Make it easier", size="sm") | |
| harder_btn = gr.Button("🔥 Make it harder", size="sm") | |
| submit_btn = gr.Button("Submit", variant="primary") | |
| verdict = gr.Markdown("") | |
| followup = gr.Markdown("") | |
| with gr.Row(): | |
| next_btn = gr.Button("Next question →", variant="primary") | |
| finish_btn = gr.Button("🏁 Finish & see recap") | |
| # Recap view | |
| with gr.Group(visible=False) as recap_view: | |
| recap_md = gr.Markdown("") | |
| resume_btn = gr.Button("↩️ Keep studying") | |
| # Debug panel — visible after each upload so you can verify extraction & chunks | |
| with gr.Accordion("🔍 Debug: extraction & chunks", open=False, visible=False) as debug_accordion: | |
| debug_data = gr.JSON(label="Content pipeline output") | |
| # Every handler that can hit the model shows a spinner (show_progress="full") | |
| # and disables its button while working, so a slow call never reads as a dead | |
| # or double-clickable screen. | |
| start_btn.click( | |
| lambda: gr.update(value="⏳ Generating deck…", interactive=False), None, start_btn, | |
| ).then( | |
| start_session, [file_in, text_in], | |
| [session, upload_view, study_view, upload_msg, progress, question, | |
| debug_accordion, debug_data], | |
| show_progress="full", | |
| ).then( | |
| lambda: gr.update(value="Generate deck & start", interactive=True), None, start_btn, | |
| ) | |
| submit_btn.click( | |
| lambda: gr.update(value="⏳ Grading…", interactive=False), None, submit_btn, | |
| ).then( | |
| submit_answer, [session, answer_in], | |
| [session, verdict, followup, progress, next_btn], | |
| show_progress="full", | |
| ).then( | |
| lambda: gr.update(value="Submit", interactive=True), None, submit_btn, | |
| ) | |
| next_btn.click( | |
| show_next, [session], | |
| [session, question, study_view, recap_view, recap_md], | |
| show_progress="full", | |
| ).then(lambda: ("", "", ""), None, [answer_in, verdict, followup]) | |
| finish_btn.click( | |
| finish_session, [session], | |
| [study_view, recap_view, recap_md], | |
| show_progress="full", | |
| ) | |
| resume_btn.click( | |
| resume_study, [session], | |
| [study_view, recap_view, question], | |
| ).then(lambda: ("", "", ""), None, [answer_in, verdict, followup]) | |
| # Difficulty dial — regenerate the current card harder/easier (NAH-32). | |
| easier_btn.click( | |
| lambda s: change_difficulty(s, "easier"), [session], | |
| [session, question, followup], show_progress="full", | |
| ) | |
| harder_btn.click( | |
| lambda s: change_difficulty(s, "harder"), [session], | |
| [session, question, followup], show_progress="full", | |
| ) | |
| if __name__ == "__main__": | |
| # Gradio 6 moved `theme` from the Blocks constructor to launch(). | |
| demo.launch(theme=gr.themes.Soft()) | |