#!/usr/bin/env python3 """ AI Teacher Bot - Single Panel UI (fixed example-evaluation & progression) """ import gradio as gr import io from contextlib import redirect_stdout from main import LEVELS, check_api_key, generate_curriculum from user_state import UserState from agents.level_assess import LevelAssessmentAgent from agents.teacher import TeacherAgent from agents.bloom_assess import BloomsAssessmentAgent import json # Global session current_session = { "user": None, "curriculum": None, "chapter_idx": 0, "module_idx": 0, "mode": None, # modes: None/idle, "assessment", "teaching", "awaiting_example", "module_passed", "bloom", "done" "questions": [], "answers": [], "q_idx": 0, "bloom_level": None, # assessment flow controls "correct_count": 0, "assessment_feedback": [], # list of dicts per question with correctness and feedback "last_feedback": None, # stores last question, answer, evaluation, reasoning for challenging "challenge_chat_history": [], # list of [user_msg, assistant_msg] pairs for challenge discussion "challenge_exchanges": 0, # count of challenge exchanges (max 3) "challenge_mode": False, # whether challenge chat is active "last_output": "" } BLOOM_ORDER = ["remember", "understand", "apply", "analyze", "evaluate", "create"] def update_main_output(text): current_session["last_output"] = text return text # ------------------------------ # Chatbot helpers (Gradio 6 safe) # ------------------------------ def empty_chat(): """ Returns empty chat in messages format (dictionaries with 'role' and 'content' keys). """ return [{"role": "assistant", "content": " "}] def safe_chat(chat): """ Ensures chat history is always valid. Uses messages format (list of dicts with 'role' and 'content'). """ if not isinstance(chat, list): return empty_chat() if len(chat) == 0: return empty_chat() # Ensure all items are dicts with role and content result = [] for item in chat: if isinstance(item, dict) and "role" in item and "content" in item: result.append(item) elif isinstance(item, tuple) and len(item) == 2: # Convert tuple (user_msg, assistant_msg) to dict format result.append({"role": "user", "content": item[0]}) result.append({"role": "assistant", "content": item[1]}) return result if result else empty_chat() def get_output_update(): return gr.update(value=current_session.get("last_output", "")) # ------------------------------ # Session & Flow helpers # ------------------------------ def reset_session_state(): current_session.update({ "user": None, "curriculum": None, "chapter_idx": 0, "module_idx": 0, "mode": None, "questions": [], "answers": [], "q_idx": 0, "bloom_level": None, "correct_count": 0, "assessment_feedback": [], "last_feedback": None, "challenge_chat_history": [], "challenge_exchanges": 0, "challenge_mode": False, "last_output": "" }) def start_learning_session(topic, claimed_level): if not topic or not topic.strip(): return "❌ Please enter a topic" if not check_api_key(): return "❌ OpenAI API key not configured! Please set OPENAI_API_KEY in your .env file." try: reset_session_state() user = UserState(topic=topic.strip(), claimed_level=claimed_level) current_session["user"] = user # Generate curriculum (level may be updated later after assessment) curriculum = generate_curriculum(user.topic, claimed_level) if curriculum is None: return "❌ Failed to generate curriculum. Please try again or check your API key." current_session["curriculum"] = curriculum except Exception as e: return f"❌ Error starting session: {str(e)}. Please try again." if claimed_level == "novice": user.set_actual_level("novice") current_session["mode"] = None # ready to start teaching return update_main_output(show_curriculum(curriculum) + "\n\nType 'next' to start Module 1.") else: # start level assessment assessor = LevelAssessmentAgent() q_text = assessor.generate_questions(user.topic, claimed_level) questions = [q.strip() for q in q_text.split("\n") if q.strip() and q[0].isdigit()] if not questions: # fallback: skip assessment user.set_actual_level(claimed_level) current_session["mode"] = None return update_main_output(show_curriculum(curriculum) + "\n\nType 'next' to start Module 1.") current_session.update({ "mode": "assessment", "questions": questions, "q_idx": 0, "answers": [], "correct_count": 0, "assessment_feedback": [], "last_feedback": None, # Will be set after first answer submission "challenge_chat_history": [], "challenge_exchanges": 0, "challenge_mode": False }) # Button should be hidden initially (no feedback yet), will show after first answer return update_main_output(f"πŸ“ LEVEL ASSESSMENT\n\nQuestion 1 of {len(questions)}:\n{questions[0]}\n\nPlease submit your answer.") # ------------------------------ # Level assessment handlers # ------------------------------ def _strip_number_prefix(q_line: str) -> str: # Converts "1. Question" -> "Question" safely q = q_line.strip() if ". " in q: parts = q.split(". ", 1) if parts[0].isdigit(): return parts[1] return q def handle_assessment(answer): qs = current_session["questions"] idx = current_session["q_idx"] if not qs: return "⚠️ No assessment in progress." question_full = qs[idx] question = _strip_number_prefix(question_full) # guard on empty/very short answers user_answer = (answer or "").strip() if len(user_answer.split()) < 5: # Don't set last_feedback for invalid answers, so button won't show return "❌ Your answer is too brief. Please provide a more detailed and specific response (at least 5 words or 1-2 sentences)." assessor = LevelAssessmentAgent() try: raw = assessor.evaluate_answer( current_session["user"].topic, current_session["user"].claimed_level, question, user_answer, ) import json as _json parsed = _json.loads(raw) evaluation = str(parsed.get("evaluation", "incorrect")).lower() reasoning = parsed.get("reasoning", "") hint = parsed.get("hint", "") # Extract hint for completeness except Exception as e: # Better error handling evaluation = "correct" if len(user_answer.split()) >= 20 else "incorrect" reasoning = f"Heuristic grading fallback used. (Error: {str(e)})" hint = "" # Record attempt current_session["answers"].append(user_answer) is_correct = evaluation == "correct" if is_correct: current_session["correct_count"] += 1 # Save per-question feedback current_session["assessment_feedback"].append({ "question": question, "correct": is_correct, "reason": reasoning, "hint": hint, }) # Store last feedback for challenging current_session["last_feedback"] = { "question": question, "answer": user_answer, "evaluation": evaluation, "reasoning": reasoning, "is_correct": is_correct, "hint": hint } # Advance to next question or finish if idx + 1 < len(qs): current_session["q_idx"] += 1 status = "βœ… Correct!" if is_correct else "❌ Incorrect." extra = f"\nReason: {reasoning}" if reasoning else "" hint_text = f"\nπŸ’‘ Hint: {hint}" if hint and not is_correct else "" return ( f"{status}{extra}{hint_text}\n\n" f"Question {current_session['q_idx']+1} of {len(qs)}:\n{qs[current_session['q_idx']]}" ) else: # Finish: compute score and assign level based on claimed level thresholds total = len(qs) score_pct = (current_session["correct_count"] / total) * 100 claimed = current_session["user"].claimed_level if claimed == "advanced": if score_pct >= 70: assigned = "advanced" elif score_pct >= 65: assigned = "intermediate" else: assigned = "novice" elif claimed == "intermediate": assigned = "intermediate" if score_pct >= 65 else "novice" else: assigned = "novice" # Build full feedback summary lines = [ "πŸ§ͺ Assessment Feedback:", ] for i, fb in enumerate(current_session["assessment_feedback"], 1): tag = "βœ…" if fb["correct"] else "❌" line = f"{tag} Q{i}: {fb['question']}" if fb.get("reason"): line += f"\n Reason: {fb['reason']}" lines.append(line) lines.append("") lines.append(f"Score: {score_pct:.1f}% | Assigned Level: {assigned}") current_session["user"].set_actual_level(assigned) # Prepare curriculum but show it on next screen curriculum = generate_curriculum(current_session["user"].topic, assigned) if curriculum: current_session["curriculum"] = curriculum feedback_text = "\n".join(lines) current_session["mode"] = "assessment_summary" current_session["last_feedback"] = None # Clear last feedback when assessment completes return feedback_text + "\n\n➑️ Press 'Next' to view your personalized curriculum." # ------------------------------ # Teaching helpers # ------------------------------ def show_curriculum(curriculum): txt = f"πŸ“– CURRICULUM FOR {current_session['user'].topic.upper()}\n" + "="*40 + "\n" for i, ch in enumerate(curriculum.chapters, 1): txt += f"\nChapter {i}: {ch.name}\n" for j, mod in enumerate(ch.modules, 1): txt += f" {i}.{j} {mod.name}\n" if getattr(mod, "learning_objective", None): txt += f" β†’ {mod.learning_objective}\n" return txt def next_step(_): mode = current_session["mode"] # If module just passed, Next moves to next module if mode == "module_passed": # advance module index now current_session["module_idx"] += 1 current_session["mode"] = None return update_main_output(start_teaching_module()) # If idle/none β†’ start teaching module if mode is None: return update_main_output(start_teaching_module()) if mode == "teaching": return update_main_output(get_explanation()) if mode == "awaiting_example": return update_main_output("βœ‹ Please submit your example using 'Submit Answer' before moving on.") if mode == "bloom": return update_main_output("🌸 Bloom assessment in progress β€” answer the Bloom question or submit to retry.") if mode == "assessment_summary": # show curriculum now and transition to normal teaching flow current_session["mode"] = None return update_main_output(show_curriculum(current_session["curriculum"]) + "\n\nType 'next' to start Module 1.") return update_main_output("⚠️ Invalid state.") def start_teaching_module(): if not current_session.get("curriculum"): return "⚠️ No curriculum loaded. Please start a session first." try: cur = current_session["curriculum"] ch_i = current_session["chapter_idx"] m_i = current_session["module_idx"] if ch_i >= len(cur.chapters): current_session["mode"] = "done" return "πŸŽ‰ You have completed the entire curriculum!" chapter = cur.chapters[ch_i] # if all modules finished -> start Bloom for the chapter if m_i >= len(chapter.modules): return start_bloom_assessment() module = chapter.modules[m_i] current_session["mode"] = "teaching" return f"πŸ“š Chapter {ch_i+1}: {chapter.name}\nModule {ch_i+1}.{m_i+1}: {module.name}\n\nObjective: {getattr(module,'learning_objective','')}\n\nClick 'Next' to get the explanation." except Exception as e: return f"❌ Error starting teaching module: {str(e)}" def get_explanation(): if not current_session.get("curriculum") or not current_session.get("user"): return "⚠️ Session not properly initialized. Please start a new session." try: cur = current_session["curriculum"] ch_i = current_session["chapter_idx"] m_i = current_session["module_idx"] chapter = cur.chapters[ch_i] # Guard if m_i >= len(chapter.modules): return start_bloom_assessment() module = chapter.modules[m_i] teacher = TeacherAgent(current_session["user"].actual_level or current_session["user"].claimed_level) explanation = teacher.teach_module(module) # returns string current_session["mode"] = "awaiting_example" return f"πŸ“– Explanation for {module.name}\n\n{explanation}\n\n✍️ Now submit your example in the box and click 'Submit Answer'." except Exception as e: return f"❌ Error getting explanation: {str(e)}" # ------------------------------ # Submit handler (single entry point wired to Submit button) # ------------------------------ def submit_answer(answer): mode = current_session.get("mode") if mode == "assessment": response = handle_assessment(answer) elif mode == "awaiting_example": response = handle_example_submission(answer) elif mode == "bloom": response = handle_bloom(answer) else: response = "⚠️ Nothing to submit right now. Click 'Next' to proceed." current_session["last_output"] = response # Return output + clear input return response, "" def start_challenge_discussion(): """ Opens the challenge discussion chat interface as a modal pop-up. Shows the original feedback and prompts user to start discussion. """ mode = current_session.get("mode") if mode != "assessment": return gr.update(visible=False), [], "⚠️ Challenge is only available during assessment.", get_output_update() last_fb = current_session.get("last_feedback") if not last_fb: return gr.update(visible=False), [], "⚠️ No feedback available to challenge. Please submit an answer first.", get_output_update() # Initialize challenge discussion current_session["challenge_mode"] = True current_session["challenge_exchanges"] = 0 current_session["challenge_chat_history"] = [] # Show initial context initial_greeting = ( f"**Grader's Original Feedback:**\n" f"Evaluation: {'βœ… Correct' if last_fb['is_correct'] else '❌ Incorrect'}\n" f"Reasoning: {last_fb['reasoning']}\n\n" f"**Question:** {last_fb['question']}\n" f"**Your Answer:** {last_fb['answer']}\n\n" f"πŸ’¬ You can now present your arguments. You have up to 3 exchanges with the grader." ) # Use messages format: list of dicts with 'role' and 'content' chat_history = [{"role": "assistant", "content": initial_greeting}] status_msg = "πŸ’¬ Challenge discussion opened. Present your first argument below (3 exchanges remaining)." return ( gr.update(visible=True), chat_history, status_msg, get_output_update() ) def handle_challenge_message(message, chat_history): """ Handles a message in the challenge discussion. Limits to 3 total exchanges (student messages). Returns: chat_history, status_msg, msg_enabled, btn_enabled, output_update """ if not current_session.get("challenge_mode"): return chat_history, "⚠️ Challenge discussion is not active.", False, False, get_output_update() if not message or not message.strip(): return chat_history, "", True, True, gr.update() if current_session["challenge_exchanges"] >= 3: return chat_history, "⚠️ Maximum exchanges (3) reached. Discussion closed. Click 'Close Discussion' to continue.", False, False, get_output_update() last_fb = current_session.get("last_feedback") if not last_fb: return chat_history, "⚠️ No feedback available.", False, False, get_output_update() assessor = LevelAssessmentAgent() try: grader_response = assessor.challenge_discussion( current_session["user"].topic, current_session["user"].claimed_level, last_fb["question"], last_fb["answer"], last_fb["evaluation"], last_fb["reasoning"], current_session["challenge_chat_history"], message ) current_session["challenge_chat_history"].append([message, grader_response]) current_session["challenge_exchanges"] += 1 # Ensure chat_history is in messages format (list of dicts) chat_history = safe_chat(chat_history) # Add the new messages in messages format chat_history.append({"role": "user", "content": message}) chat_history.append({"role": "assistant", "content": grader_response}) remaining = 3 - current_session["challenge_exchanges"] if remaining > 0: status_msg = f"πŸ’¬ {remaining} exchange(s) remaining. You can continue the discussion." output_update = gr.update() msg_enabled = True btn_enabled = True else: # Finalize evaluation after 3 exchanges status_msg, output_update = finalize_challenge_discussion(grader_response) msg_enabled = False btn_enabled = False return chat_history, status_msg, msg_enabled, btn_enabled, output_update except Exception as e: return chat_history, f"❌ Error: {str(e)}", True, True, gr.update() def finalize_challenge_discussion(final_response): """ Finalizes the challenge discussion and updates evaluation if needed. Extracts final evaluation from the last grader response. Returns: (status_msg, main_output_update) """ last_fb = current_session.get("last_feedback") if not last_fb: return "⚠️ Could not finalize challenge.", get_output_update() # Try to extract evaluation from the final response # Use the challenge_feedback method to get a structured final evaluation assessor = LevelAssessmentAgent() try: # Get the full conversation context conversation_text = "\n".join([ f"Student: {msg[0]}\nGrader: {msg[1]}" for msg in current_session["challenge_chat_history"] ]) # Final re-evaluation request final_prompt = ( f"Based on our discussion:\n{conversation_text}\n\n" "Please provide your FINAL evaluation as JSON with: " '{"evaluation": "correct" or "incorrect", "reasoning": "explanation", "original_was_fair": true/false}' ) raw = assessor.challenge_feedback( current_session["user"].topic, current_session["user"].claimed_level, last_fb["question"], last_fb["answer"], last_fb["evaluation"], last_fb["reasoning"] ) import json as _json parsed = _json.loads(raw) new_evaluation = str(parsed.get("evaluation", "incorrect")).lower() new_reasoning = parsed.get("reasoning", "") original_was_fair = parsed.get("original_was_fair", True) new_is_correct = new_evaluation == "correct" old_is_correct = last_fb["is_correct"] # Build the main output update qs = current_session["questions"] idx = current_session["q_idx"] current_question_text = "" if idx < len(qs): current_question_text = f"\n\nπŸ“ Current Question {idx+1} of {len(qs)}:\n{qs[idx]}" # Update if evaluation changed if new_is_correct != old_is_correct: if new_is_correct and not old_is_correct: current_session["correct_count"] += 1 elif not new_is_correct and old_is_correct: current_session["correct_count"] = max(0, current_session["correct_count"] - 1) if current_session["assessment_feedback"]: current_session["assessment_feedback"][-1]["correct"] = new_is_correct current_session["assessment_feedback"][-1]["reason"] = new_reasoning current_session["last_feedback"]["evaluation"] = new_evaluation current_session["last_feedback"]["reasoning"] = new_reasoning current_session["last_feedback"]["is_correct"] = new_is_correct # Build updated main output main_output = ( f"πŸ”„ **EVALUATION UPDATED AFTER CHALLENGE**\n\n" f"**Question:** {last_fb['question']}\n" f"**Your Answer:** {last_fb['answer']}\n\n" f"**Original Evaluation:** {'βœ… Correct' if old_is_correct else '❌ Incorrect'}\n" f"**Updated Evaluation:** {'βœ… Correct' if new_is_correct else '❌ Incorrect'}\n\n" f"**Updated Reasoning:** {new_reasoning}\n" f"{current_question_text}" ) status_msg = "πŸ”„ **FINAL EVALUATION UPDATED**\n\n" + \ f"**New Evaluation:** {'βœ… Correct' if new_is_correct else '❌ Incorrect'}\n" + \ f"**Final Reasoning:** {new_reasoning}\n\n" + \ "βœ… Challenge discussion completed. Evaluation has been updated on the main screen." else: # Build main output showing final evaluation main_output = ( f"πŸ“‹ **FINAL EVALUATION AFTER CHALLENGE**\n\n" f"**Question:** {last_fb['question']}\n" f"**Your Answer:** {last_fb['answer']}\n\n" f"**Evaluation:** {'βœ… Correct' if new_is_correct else '❌ Incorrect'} (unchanged)\n\n" f"**Final Reasoning:** {new_reasoning}\n" f"{current_question_text}" ) status_msg = "πŸ“‹ **FINAL EVALUATION**\n\n" + \ f"**Evaluation:** {'βœ… Correct' if new_is_correct else '❌ Incorrect'} (unchanged)\n" + \ f"**Final Reasoning:** {new_reasoning}\n\n" + \ "βœ… Challenge discussion completed. You may continue with the assessment." # Close challenge mode current_session["challenge_mode"] = False current_session["last_output"] = main_output return status_msg, gr.update(value=main_output) except Exception as e: current_session["challenge_mode"] = False error_msg = f"⚠️ Error finalizing challenge: {str(e)}" return error_msg, get_output_update() def handle_example_submission(example_text): """ Uses TeacherAgent.evaluate_example(module, example) to decide correctness. If correct -> set mode to 'module_passed' and require user to press Next to move on. If incorrect -> remain in 'awaiting_example' and show feedback. """ if not current_session.get("curriculum"): return "⚠️ No curriculum loaded. Please start a session first." cur = current_session["curriculum"] ch_i = current_session["chapter_idx"] m_i = current_session["module_idx"] if ch_i >= len(cur.chapters): return "⚠️ No active chapter available." chapter = cur.chapters[ch_i] if m_i >= len(chapter.modules): # Shouldn't happen, but guard return "⚠️ No active module to evaluate." module = chapter.modules[m_i] teacher = TeacherAgent(current_session["user"].actual_level or current_session["user"].claimed_level) # quick client-side guardrails for empty/brief examples if not example_text or not str(example_text).strip(): return "❌ Please provide an example to demonstrate your understanding." if len(str(example_text).strip().split()) < 10: return "❌ Your example is too brief. Provide 2-3 sentences with specific details." try: eval_result = teacher.evaluate_example(module, example_text) is_correct = bool(eval_result.get("is_correct")) feedback = eval_result.get("feedback", "No feedback provided.") confidence = eval_result.get("confidence", None) except Exception as e: return f"❌ Error evaluating example: {str(e)}. Please try again." if is_correct: # mark module as passed (do not auto-increment module_idx β€” require Next) current_session["mode"] = "module_passed" return f"βœ… Example accepted. Feedback: {feedback}\n\n➑️ Click 'Next' to continue to the next module." else: # remain in awaiting_example β€” must retry current_session["mode"] = "awaiting_example" return f"❌ Example not sufficient. Feedback: {feedback}\n\nPlease try another example for the same module." # ------------------------------ # Bloom assessment (per chapter) # ------------------------------ def start_bloom_assessment(): if not current_session.get("curriculum"): return "⚠️ No curriculum loaded. Please start a session first." try: current_session["mode"] = "bloom" current_session["bloom_level"] = BLOOM_ORDER[0] chapter = current_session["curriculum"].chapters[current_session["chapter_idx"]] return ask_bloom_question(current_session["bloom_level"], chapter) except Exception as e: return f"❌ Error starting Bloom assessment: {str(e)}" def ask_bloom_question(level, chapter): try: agent = BloomsAssessmentAgent() # agent.generate_bloom_question expects (chapter, bloom_level) q = agent.generate_bloom_question(chapter, level) current_session["questions"] = [q] return f"🌸 Bloom's Assessment ({level.title()}) for Chapter: {chapter.name}\n\n{q}\n\n✍️ Answer below and click Submit." except Exception as e: return f"❌ Error generating Bloom question: {str(e)}" def handle_bloom(answer): if not current_session.get("curriculum"): return "⚠️ No curriculum loaded. Please start a session first." if not answer or not str(answer).strip(): return "❌ Please provide an answer for the Bloom assessment question." try: level = current_session["bloom_level"] if not current_session["questions"]: return "⚠️ No question available. Please start a new session." question = current_session["questions"][0] chapter = current_session["curriculum"].chapters[current_session["chapter_idx"]] agent = BloomsAssessmentAgent() # agent.evaluate_bloom_answer returns a JSON string per your agent implementation raw_eval = agent.evaluate_bloom_answer(question, answer, level, chapter) # parse evaluation JSON try: eval_obj = json.loads(raw_eval) score = float(eval_obj.get("score", 0)) feedback = str(eval_obj.get("feedback", "No feedback")) except (json.JSONDecodeError, ValueError) as e: # fallback: if text contains 'correct' treat as pass feedback = f"Could not parse evaluation response. Raw: {raw_eval[:100]}..." score = 10 if "correct" in raw_eval.lower() else 0 except KeyError as e: return f"❌ Session error: Missing required data ({str(e)}). Please restart the session." except Exception as e: return f"❌ Error evaluating Bloom answer: {str(e)}. Please try again." # use threshold (e.g., >=6/10) if score >= 6: # advance bloom level next_idx = BLOOM_ORDER.index(level) + 1 if next_idx < len(BLOOM_ORDER): current_session["bloom_level"] = BLOOM_ORDER[next_idx] # generate new question for next level return f"βœ… {feedback}\n\n➑️ Moving to {BLOOM_ORDER[next_idx].title()}.\n\n" + ask_bloom_question(current_session["bloom_level"], chapter) else: # finished Bloom for chapter -> next chapter current_session["chapter_idx"] += 1 current_session["module_idx"] = 0 current_session["mode"] = None return f"πŸŽ‰ {feedback}\n\nβœ… Bloom’s assessment completed for chapter '{chapter.name}'.\nType 'next' to continue." else: # ask a new question for same level return f"❌ {feedback}\n\nπŸ” Try another question at the same level.\n\n" + ask_bloom_question(level, chapter) # ------------------------------ # Gradio UI wiring (single unified output) # ------------------------------ # Custom CSS for modal pop-up modal_css = """ .modal-overlay:not([style*="display: none"]) { position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; background-color: rgba(0, 0, 0, 0.5) !important; z-index: 1000 !important; display: flex !important; align-items: center !important; justify-content: center !important; padding: 20px !important; } /* When Gradio hides the element, ensure it doesn't block interactions */ .modal-overlay[style*="display: none"], .modal-overlay[style*="display:none"] { display: none !important; visibility: hidden !important; pointer-events: none !important; z-index: -1 !important; opacity: 0 !important; } .modal-content { background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important; border-radius: 10px !important; padding: 20px !important; max-width: 900px !important; width: 100% !important; max-height: 90vh !important; overflow-y: auto !important; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3) !important; } /* Chat window styling */ .modal-content .gradio-chatbot { background-color: #ffffff !important; border-radius: 8px !important; padding: 15px !important; border: 2px solid #e0e0e0 !important; } .modal-content .gradio-chatbot .message { background-color: #f8f9fa !important; border-radius: 8px !important; padding: 10px !important; margin: 5px 0 !important; } .modal-content .gradio-chatbot .user-message { background-color: #e3f2fd !important; border-left: 4px solid #2196f3 !important; } .modal-content .gradio-chatbot .bot-message { background-color: #f1f8e9 !important; border-left: 4px solid #8bc34a !important; } .modal-header { margin: 0 !important; padding: 0 !important; flex-grow: 1 !important; } .modal-close-btn { min-width: 40px !important; height: 40px !important; border-radius: 50% !important; font-size: 20px !important; font-weight: bold !important; } .modal-note { font-size: 12px !important; color: #666 !important; margin-top: 10px !important; } """ with gr.Blocks(title="AI Teacher Bot") as demo: gr.Markdown(""" # 🧠 AI Teacher Bot In this interactive learning experience, you will be prompted to select your learning levelβ€”**Beginner**, **Intermediate**, or **Advanced**. - If you choose **Intermediate** or **Advanced**, you will be presented with an assessment designed to test your understanding of the material. The questions in these assessments are tailored to the selected level, ensuring they are **challenging and reflective of the knowledge expected at that stage**. For instance, **Intermediate-level questions** will require more in-depth explanations, not just basic one-liner answers. Similarly, **Advanced-level assessments** will be comprehensive and demand a higher level of critical thinking and subject mastery. """) with gr.Row(): topic = gr.Textbox(label="πŸ“ Topic", placeholder="e.g., Python Programming", scale=2) level = gr.Dropdown(choices=LEVELS, value="novice", label="πŸŽ“ Your Level", scale=1) start_btn = gr.Button("πŸš€ Start Session") output = gr.Textbox(label="πŸ“š Session Output", lines=25, interactive=False, autoscroll=True) answer_box = gr.Textbox(label="✍️ Your Answer / Example", placeholder="Type your answer...", lines=5, max_lines=10) with gr.Row(): submit_btn = gr.Button("Submit Answer") next_btn = gr.Button("Next") challenge_btn = gr.Button("Challenge Assessment", visible=False) # Challenge discussion chat interface - Modal Pop-up with gr.Column(visible=False, elem_classes="modal-overlay") as challenge_modal_overlay: with gr.Column(elem_classes="modal-content"): with gr.Row(): gr.Markdown("### πŸ’¬ Challenge Discussion with Grader", elem_classes="modal-header") challenge_close_btn = gr.Button("βœ•", elem_classes="modal-close-btn", scale=0) with gr.Row(): with gr.Column(scale=3): challenge_chat = gr.Chatbot( label="", height=400, show_label=False, container=True ) challenge_status = gr.Textbox( label="Status", interactive=False, lines=2, container=True ) with gr.Column(scale=1): challenge_msg_box = gr.Textbox( label="Your Argument/Question", placeholder="Explain why you think your answer is correct...", lines=5, container=True ) challenge_send_btn = gr.Button("Send", variant="primary") gr.Markdown("**Note:** You have up to 3 exchanges with the grader.", elem_classes="modal-note") # Function to update challenge button visibility def update_challenge_visibility(): mode = current_session.get("mode") has_feedback = current_session.get("last_feedback") is not None # Show button during assessment mode after first answer is submitted should_show = (mode == "assessment" and has_feedback) return gr.update(visible=should_show) # handlers start_btn.click( fn=start_learning_session, inputs=[topic, level], outputs=[output] ).then( fn=update_challenge_visibility, inputs=None, outputs=[challenge_btn] ) submit_btn.click( fn=submit_answer, inputs=[answer_box], outputs=[output, answer_box] ).then( fn=update_challenge_visibility, inputs=None, outputs=[challenge_btn] ) next_btn.click( fn=next_step, inputs=[answer_box], outputs=[output] ).then( fn=update_challenge_visibility, inputs=None, outputs=[challenge_btn] ) challenge_btn.click( fn=start_challenge_discussion, inputs=None, outputs=[challenge_modal_overlay, challenge_chat, challenge_status, output] ).then( fn=lambda: (gr.update(value="", interactive=True), gr.update(interactive=True)), inputs=None, outputs=[challenge_msg_box, challenge_send_btn] ) def send_and_clear(message, chat_history): """Send message and clear input box (if needed)""" history, status, msg_enabled, btn_enabled, output_update = handle_challenge_message(message, chat_history) if message and message.strip(): msg_update = gr.update(value="", interactive=msg_enabled) else: msg_update = gr.update(value=message, interactive=msg_enabled) btn_update = gr.update(interactive=btn_enabled) return history, msg_update, status, btn_update, output_update challenge_send_btn.click( fn=send_and_clear, inputs=[challenge_msg_box, challenge_chat], outputs=[challenge_chat, challenge_msg_box, challenge_status, challenge_send_btn, output] ) challenge_msg_box.submit( fn=send_and_clear, inputs=[challenge_msg_box, challenge_chat], outputs=[challenge_chat, challenge_msg_box, challenge_status, challenge_send_btn, output] ) def close_challenge_discussion(): """ Properly closes the challenge modal and restores UI control """ # Reset backend state current_session["challenge_mode"] = False current_session["challenge_chat_history"] = [] current_session["challenge_exchanges"] = 0 # IMPORTANT: Use visible=False to hide the overlay completely # This should remove it from the DOM or set display:none which CSS will respect return ( gr.update(visible=False), # Hide overlay - this should remove blocking empty_chat(), # Reset chat "βœ… Challenge closed.", # Status message gr.update(value=current_session.get("last_output", "")) # Keep main output ) challenge_close_btn.click( fn=close_challenge_discussion, inputs=None, outputs=[ challenge_modal_overlay, # visible=False challenge_chat, # reset chat challenge_status, # status text output # main output (reevaluated decision) ] ).then( fn=update_challenge_visibility, inputs=None, outputs=[challenge_btn] ) if __name__ == "__main__": demo.launch( share=True, server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft(), css=modal_css )