import gradio as gr import os import torch from transformers import AutoModelForCausalLM, AutoTokenizer from accelerate import Accelerator import re import time import PyPDF2 import sqlite3 import json # Import json for pretty printing parsed data # --- Global Constants --- MAX_QUIZ_QUESTIONS_UI = 20 # Define this globally so UI can access it WEAK_THRESHOLD_PERCENTAGE = 65 # Percentage below which a topic is considered a weak area quiz_context = {} # --- Database Setup --- DB_NAME = 'edututor.db' def init_db(): conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS quiz_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, topic TEXT NOT NULL, difficulty TEXT NOT NULL, score INTEGER, num_questions INTEGER ) ''') conn.commit() conn.close() print(f"Database '{DB_NAME}' initialized successfully.") def record_quiz_result(topic, difficulty, score, num_questions): conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() timestamp = time.strftime('%Y-%m-%d %H:%M:%S') cursor.execute(''' INSERT INTO quiz_results (timestamp, topic, difficulty, score, num_questions) VALUES (?, ?, ?, ?, ?) ''', (timestamp, topic, difficulty, score, num_questions)) conn.commit() conn.close() print(f"Recorded quiz result: Topic='{topic}', Difficulty='{difficulty}', Score={score}/{num_questions}") def get_performance_data(): """Fetches raw performance data from the database.""" conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() cursor.execute('SELECT timestamp, topic, difficulty, score, num_questions FROM quiz_results ORDER BY timestamp DESC') data = cursor.fetchall() conn.close() return data def get_performance_insights(): """ Analyzes quiz performance data and generates markdown for weak areas and suggested next steps, along with the dataframe for history. """ performance_data = get_performance_data() # Fetch raw data # We return the actual data for the DataFrame, Gradio will handle updating its value df_data_for_update = performance_data if not performance_data: # Return empty data for DataFrame, and messages for markdown return [], "No quiz data available for analysis. Please take some quizzes first!", "No specific suggestions at this time." topic_scores = {} # {topic: [score_percentage, ...]} for timestamp, topic, difficulty, score, num_questions in performance_data: if num_questions > 0: percentage = (score / num_questions) * 100 if topic not in topic_scores: topic_scores[topic] = [] topic_scores[topic].append(percentage) weak_areas_list = [] for topic, percentages in topic_scores.items(): avg_percentage = sum(percentages) / len(percentages) num_quizzes = len(percentages) if avg_percentage < WEAK_THRESHOLD_PERCENTAGE: weak_areas_list.append({ "topic": topic, "avg_score_percentage": avg_percentage, "num_quizzes": num_quizzes }) # Sort weak areas by average score (lowest first) weak_areas_list.sort(key=lambda x: x['avg_score_percentage']) # --- Weak Areas Markdown --- weak_areas_markdown = "" if not weak_areas_list: weak_areas_markdown = "š **Excellent work!** No significant weak areas detected based on your quiz performance. Keep up the great work!" else: weak_areas_markdown = "### Based on your performance, here are some areas where you might need more practice:\n\n" for area in weak_areas_list: weak_areas_markdown += f"- **{area['topic']}**: Average score of **{area['avg_score_percentage']:.1f}%** across {area['num_quizzes']} quiz(es).\n" # --- Suggested Steps Markdown --- suggested_steps_markdown = "" if weak_areas_list: suggested_steps_markdown += "### Suggested Next Steps:\n\n" suggested_steps_markdown += "š **Focus on these topics:**\n" for area in weak_areas_list: suggested_steps_markdown += f"- Try generating a new quiz specifically on **{area['topic']}** (e.g., using the 'Generate Quiz' tab).\n" suggested_steps_markdown += "\nš” Practice makes perfect! Revisit relevant materials and re-take quizzes on these subjects." else: suggested_steps_markdown = "### Suggested Next Steps:\n\n" suggested_steps_markdown += "Keep exploring new topics or challenge yourself with a 'hard' difficulty quiz! You're doing great!" return df_data_for_update, weak_areas_markdown, suggested_steps_markdown # --- Model Loading (Local Inference) --- GRANITE_MODEL_NAME = "ibm-granite/granite-3.3-2b-instruct" tokenizer = None model = None device = "cuda" if torch.cuda.is_available() else "cpu" def load_granite_model(): global tokenizer, model if model is not None and tokenizer is not None: return tokenizer, model print(f"Loading {GRANITE_MODEL_NAME} on {device}...") try: tokenizer = AutoTokenizer.from_pretrained(GRANITE_MODEL_NAME) model = AutoModelForCausalLM.from_pretrained( GRANITE_MODEL_NAME, device_map="auto", torch_dtype=torch.float16 ) model.eval() print(f"Successfully loaded model: {GRANITE_MODEL_NAME} locally!") print(f"Model is running on: {device}") return tokenizer, model except Exception as e: print(f"Error loading model {GRANITE_MODEL_NAME}: {e}") print("Possible reasons: Insufficient GPU RAM, model not found, or network issues during download.") print("Consider trying a smaller model like 'ibm-granite/granite-3.0-2b-instruct' or upgrading Colab runtime.") return None, None # Try loading the actual model, if it fails, fallback to mock objects. try: tokenizer, model = load_granite_model() if tokenizer is None or model is None: raise Exception("Model or tokenizer failed to load, falling back to mock.") except Exception as e: print(f"INFO: Failed to load actual model ({e}). Using mock model for demonstration.") class MockModel: def generate(self, input_ids, max_new_tokens, temperature, top_p, do_sample, pad_token_id, eos_token_id): text = self.tokenizer.decode(input_ids[0], skip_special_tokens=True) if "summarize" in text.lower(): return self.tokenizer.encode("This is a mock summary of your text.") elif "refine" in text.lower(): return self.tokenizer.encode("This is your mock refined text.") elif "meaning of" in text.lower(): return self.tokenizer.encode("Mock meaning: A mock object is a simulated object.") elif "translate" in text.lower(): return self.tokenizer.encode("Mock translation.") elif "quiz" in text.lower(): mock_quiz_output = """ 1. Question: What is the capital of France? A. Berlin B. Madrid C. Paris D. Rome Correct Answer: C Explanation: Paris is the capital and most populous city of France. 2. Question: Which planet is known as the Red Planet? A. Earth B. Mars C. Jupiter D. Venus Correct Answer: B Explanation: Mars is often referred to as the Red Planet because of its reddish appearance. 3. Question: What is the largest ocean on Earth? A. Atlantic Ocean B. Indian Ocean C. Arctic Ocean D. Pacific Ocean Correct Answer: D Explanation: The Pacific Ocean is the largest and deepest of Earth's five oceanic divisions. """ return self.tokenizer.encode(mock_quiz_output) return self.tokenizer.encode("Hello! This is a mock response from Edu-Tutor AI.") tokenizer = AutoTokenizer.from_pretrained("gpt2") # Using a small, easily downloadable tokenizer for mock model = MockModel() model.tokenizer = tokenizer # Attach tokenizer to mock model for encoding/decoding # Ensure accelerator is prepared even for mock model (if using torch operations) accelerator = Accelerator() # Note: For the MockModel, accelerator.prepare might not be strictly necessary if it doesn't use torch tensors internally # but keeping it for consistency if you swap to a real model. # If you get errors here with the mock model, you can remove this line for the mock setup. # model, tokenizer = accelerator.prepare(model, tokenizer) # --- Helper Functions for Model Inference --- STREAM_DELAY = 0.005 # seconds per word def generate_response(prompt, chat_history_for_model): if model is None or tokenizer is None: yield "Error: Model not loaded. Please check Colab runtime and GPU memory." return messages = [] for human, ai in chat_history_for_model: messages.append({"role": "user", "content": human if human is not None else ""}) messages.append({"role": "assistant", "content": ai if ai is not None else ""}) messages.append({"role": "user", "content": prompt}) input_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) input_ids = tokenizer(input_text, return_tensors="pt").to(device) current_response = "" try: input_token_len = input_ids.input_ids.shape[1] # Ensure pad_token_id and eos_token_id are set for generate if tokenizer.pad_token_id is None: tokenizer.pad_token_id = tokenizer.eos_token_id # Often EOS token is used as pad_token_id if tokenizer.eos_token_id is None: # Fallback if tokenizer doesn't provide one # For some models/tokenizers, you might manually set an arbitrary ID or handle it differently # For gpt2, eos_token_id is typically 50256 if hasattr(tokenizer, 'default_end_token_id'): tokenizer.eos_token_id = tokenizer.default_end_token_id elif tokenizer.vocab_size > 0: tokenizer.eos_token_id = tokenizer.vocab_size - 1 # Last token ID as a fallback full_output_tokens = model.generate( **input_ids, max_new_tokens=100, temperature=0.7, do_sample=True, pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id ) full_response = tokenizer.decode(full_output_tokens[0][input_token_len:], skip_special_tokens=True).strip() words = full_response.split(" ") for i in range(len(words)): current_response += words[i] + " " yield current_response time.sleep(STREAM_DELAY) return except Exception as e: yield f"Error during inference: {e}" # Function to read text from PDF def read_pdf_text(pdf_file_path): text = "" if pdf_file_path is None: return "" try: with open(pdf_file_path, 'rb') as file: reader = PyPDF2.PdfReader(file) for page_num in range(len(reader.pages)): page = reader.pages[page_num] text += page.extract_text() return text except Exception as e: return f"Error reading PDF: {e}" # --- Quiz Parsing Function --- def parse_quiz_text(raw_quiz_text): """ Parses the raw quiz text generated by the LLM into a structured list of dictionaries. Each dictionary represents a question. """ parsed_questions = [] # Regex to find each question block # It looks for: # 1. An optional number (e.g., "1.") # 2. " Question:" followed by the question text # 3. Lines starting with A., B., C., D. for options # 4. "Correct Answer: [Letter]" # 5. "Explanation: [Text]" question_pattern = re.compile( r'(\d*\.?\s*Q(?:uestion)?:\s*(.*?)\n' # Optional number, "Question:", then question text (group 2) r'\s*A\.\s*(.*?)\n' # Option A (group 3) r'\s*B\.\s*(.*?)\n' # Option B (group 4) r'\s*C\.\s*(.*?)\n' # Option C (group 5) r'\s*D\.\s*(.*?)\n' # Option D (group 6) r'\s*Correct Answer:\s*([A-D])\s*\n' # Correct Answer letter (group 7) r'\s*Explanation:\s*(.*?)(?=\n\d*\.?\s*Q(?:uestion)?:|\Z))', # Explanation (group 8), lookahead for next question or end of string re.DOTALL # . matches newlines ) matches = question_pattern.findall(raw_quiz_text) for match in matches: question_text = match[1].strip() options = { 'A': match[2].strip(), 'B': match[3].strip(), 'C': match[4].strip(), 'D': match[5].strip() } correct_answer = match[6].strip() explanation = match[7].strip() # Clean up question text if it has extra "Question:" or leading/trailing spaces # Also remove the leading number if present, as it's just for display initially question_text = re.sub(r'^\d+\.\s*Question:\s*', '', question_text, flags=re.IGNORECASE).strip() parsed_questions.append({ 'question_text': question_text, 'options': options, 'correct_answer': correct_answer, 'explanation': explanation }) print(f"DEBUG: Parsed {len(parsed_questions)} questions.") return parsed_questions # --- Quiz Generation and Scoring Functions --- quiz_context = { "topic": "", "difficulty": "", "parsed_quiz": [] } def generate_quiz_for_display(quiz_topic, num_quiz_questions_str, difficulty="medium", pdf_file=None): # Initialize a list of gr.update objects for all expected outputs # The first element is for parsed_quiz_data_state, and should be gr.update() updates = [ gr.update(), # 0: parsed_quiz_data_state (will be updated with actual data) - CORRECTED gr.update(visible=False, value=""), # 1: raw_quiz_output_display (clear and hide) gr.update(visible=False), # 2: submit_quiz_button gr.update(value="", visible=False), # 3: quiz_results_display gr.update(visible=False), # 4: start_new_quiz_button gr.update(visible=False) # 5: interactive_quiz_column - initially hidden for reset ] # Add updates for all possible question/radio pairs, initially hidden for i in range(MAX_QUIZ_QUESTIONS_UI): updates.append(gr.update(value="", visible=False)) # Q_md updates.append(gr.update(choices=[], value=None, visible=False)) # Q_radio # Yield initial reset state (important for clearing previous quiz display) yield tuple(updates) if model is None or tokenizer is None: print(f"DEBUG: Model or tokenizer not loaded at start of generate_quiz_for_display. Current time: {time.time()}") updates[1] = gr.update(value="Error: Model not loaded. Please check Colab runtime and GPU memory.", visible=True) yield tuple(updates) return try: num_quiz_questions = int(num_quiz_questions_str) if not (1 <= num_quiz_questions <= MAX_QUIZ_QUESTIONS_UI): raise ValueError(f"Number of questions must be between 1 and {MAX_QUIZ_QUESTIONS_UI}.") except ValueError as e: print(f"DEBUG: Invalid number of questions: {e}. Current time: {time.time()}") updates[1] = gr.update(value=f"Invalid number of questions: {e}. Please enter a whole number between 1 and {MAX_QUIZ_QUESTIONS_UI}.", visible=True) yield tuple(updates) return context_text = "" effective_topic = "" if pdf_file: context_text = read_pdf_text(pdf_file.name) if "Error reading PDF" in context_text: print(f"DEBUG: Error reading PDF: {context_text}. Current time: {time.time()}") updates[1] = gr.update(value=context_text, visible=True) yield tuple(updates) return if not context_text.strip(): print(f"DEBUG: PDF file is empty or could not be read (empty context_text). Current time: {time.time()}") updates[1] = gr.update(value="PDF file is empty or could not be read.", visible=True) yield tuple(updates) return effective_topic = f"PDF: {os.path.basename(pdf_file.name)}" elif quiz_topic and quiz_topic.strip(): context_text = quiz_topic effective_topic = quiz_topic else: print(f"DEBUG: No topic or PDF provided. Current time: {time.time()}") updates[1] = gr.update(value="Please provide a topic for the quiz or upload a PDF.", visible=True) yield tuple(updates) return if not context_text.strip(): print(f"DEBUG: Final context_text is empty after processing. Current time: {time.time()}") updates[1] = gr.update(value="A valid topic or non-empty PDF content is required to generate a quiz.", visible=True) yield tuple(updates) return difficulty_instruction = "" if difficulty == "easy": difficulty_instruction = "Make the questions relatively easy and straightforward." elif difficulty == "hard": difficulty_instruction = "Make the questions challenging and detailed, requiring deeper understanding." else: # medium difficulty_instruction = "Make the questions of medium difficulty." quiz_prompt = f"""Generate {num_quiz_questions} multiple-choice questions about the following text or topic: {context_text} {difficulty_instruction} For each question, provide exactly 4 options (A, B, C, D), clearly state the single correct answer letter, and give an **EXTREMELY brief, 1-sentence explanation. Be very concise.** Focus on **very short questions** and **short options**. No introductory or concluding remarks. Just the questions. **IMPORTANT: Prepend each "Question:" with its number (e.g., "1. Question:", "2. Question:").** Format each question STRICTLY like this, with no extra text or numbering outside this pattern: [Number]. Question: [Short question text] A. [Option A] B. [Option B] C. [Option C] D. [Option D] Correct Answer: [Letter] Explanation: [Very brief explanation, 1 sentence max, no fluff] Example: 1. Question: Capital of France? A. Berlin B. Madrid C. Paris D. Rome Correct Answer: C Explanation: Paris is the capital. """ print(f"DEBUG: Quiz prompt length: {len(quiz_prompt)}. First 200 chars: {quiz_prompt[:200]}. Current time: {time.time()}") messages = [{"role": "user", "content": quiz_prompt}] input_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) if not input_text.strip(): print(f"DEBUG: Tokenizer applied chat template resulted in empty input_text. Current time: {time.time()}") updates[1] = gr.update(value="Error: Could not create a valid prompt for the quiz. This might happen if the topic or PDF content is too short or invalid.", visible=True) yield tuple(updates) return quiz_input_ids = None try: quiz_input_ids = tokenizer(input_text, return_tensors="pt").to(device) print(f"DEBUG: Tokenized input_ids shape: {quiz_input_ids.input_ids.shape}. Current time: {time.time()}") except Exception as e: print(f"DEBUG: Error tokenizing input for quiz: {e}. Current time: {time.time()}") updates[1] = gr.update(value=f"Error tokenizing input for quiz: {e}. Please ensure input text is valid.", visible=True) yield tuple(updates) return try: MAX_TOKENS_PER_QUIZ_QUESTION = 70 quiz_max_new_tokens = num_quiz_questions * MAX_TOKENS_PER_QUIZ_QUESTION print(f"DEBUG: Generating with max_new_tokens={quiz_max_new_tokens}. Current time: {time.time()}") full_output_tokens = model.generate( **quiz_input_ids, max_new_tokens=quiz_max_new_tokens, temperature=0.7, do_sample=True, pad_token_id=tokenizer.eos_token_id ) raw_quiz_text = tokenizer.decode(full_output_tokens[0][quiz_input_ids.input_ids.shape[1]:], skip_special_tokens=True) # --- Temporarily show raw quiz text for debugging --- # You can set visible=False after testing updates[1] = gr.update(value=raw_quiz_text, visible=True, label="Raw Quiz Output (for debugging)") yield tuple(updates) # Yield this update so you can see the raw output while parsing happens print(f"DEBUG: Raw quiz text generated. Length: {len(raw_quiz_text)}. First 500 chars:\n{raw_quiz_text[:500]}. Current time: {time.time()}") parsed_quiz_data = parse_quiz_text(raw_quiz_text) print("DEBUG: Parsed quiz data:") print(json.dumps(parsed_quiz_data, indent=2)) print(f"DEBUG: Number of questions parsed: {len(parsed_quiz_data)}. Current time: {time.time()}") if not parsed_quiz_data: print(f"DEBUG: No questions parsed from the generated text. Current time: {time.time()}") updates[1] = gr.update(value="Could not generate a valid quiz with the requested format. Please try again with a different topic or fewer questions.", visible=True) yield tuple(updates) return # Store for later use by scoring function quiz_context["topic"] = effective_topic quiz_context["difficulty"] = difficulty quiz_context["parsed_quiz"] = parsed_quiz_data # Update the initial components updates[0] = gr.update(value=parsed_quiz_data) # Update gr.State with the parsed data print(f"DEBUG: Value for parsed_quiz_data_state update: {len(parsed_quiz_data)} questions.") updates[1] = gr.update(visible=False, value="") # Hide the raw text output after successful parsing updates[2] = gr.update(visible=True) # Show the submit button updates[3] = gr.update(value="", visible=False) # Clear and hide previous results updates[4] = gr.update(visible=False) # Hide start new quiz button initially updates[5] = gr.update(visible=True) # Make the interactive quiz column visible here # Populate quiz questions and options. output_idx = 6 # Start index for question/radio pairs after the initial 6 common components for i, q in enumerate(parsed_quiz_data): if i < MAX_QUIZ_QUESTIONS_UI: # Ensure we don't exceed placeholder count question_label = f"{i+1}. {q['question_text']}" choices = [f"A. {q['options']['A']}", f"B. {q['options']['B']}", f"C. {q['options']['C']}", f"D. {q['options']['D']}"] # Update question Markdown and Radio button using gr.update updates[output_idx] = gr.update(value=question_label, visible=True) updates[output_idx + 1] = gr.update(choices=choices, value=None, label=f"Select your answer for Q{i+1}", visible=True) else: # This should ideally not happen if num_quiz_questions <= MAX_QUIZ_QUESTIONS_UI # But for safety, ensure any remaining placeholders are hidden. updates[output_idx] = gr.update(value="", visible=False) updates[output_idx + 1] = gr.update(value=None, visible=False, choices=[]) output_idx += 2 # Hide any remaining unused placeholder quiz elements for i in range(len(parsed_quiz_data), MAX_QUIZ_QUESTIONS_UI): updates[output_idx] = gr.update(value="", visible=False) updates[output_idx + 1] = gr.update(value=None, visible=False, choices=[]) output_idx += 2 print(f"DEBUG: Gradio UI updates prepared for interactive quiz. Current time: {time.time()}") yield tuple(updates) # Yield the populated quiz UI except Exception as e: print(f"DEBUG: Error during quiz generation inference or parsing: {e}. Current time: {time.time()}") updates[1] = gr.update(value=f"Error generating or parsing quiz: {e}. Please check Colab console for details.", visible=True) yield tuple(updates) # Make sure quiz_context is defined globally at the top of your script, e.g.: # quiz_context = {} def submit_quiz(*user_answers_raw): # Removed parsed_quiz_data as an input argument """ Scores the quiz based on user answers and provides detailed feedback. *user_answers_raw will be a tuple where each element is the selected option string for a question. """ # --- DEBUGGING PRINTS: Access quiz_data from global quiz_context --- print(f"DEBUG: submit_quiz called.") # Get parsed quiz data directly from the global quiz_context parsed_quiz_data = quiz_context.get("parsed_quiz", []) print(f"DEBUG: Type of parsed_quiz_data (from quiz_context): {type(parsed_quiz_data)}") print(f"DEBUG: Content of parsed_quiz_data (from quiz_context): {parsed_quiz_data}") print(f"DEBUG: Length of parsed_quiz_data (from quiz_context): {len(parsed_quiz_data) if parsed_quiz_data is not None else 'None'}") # --- END DEBUGGING PRINTS --- # Prepare outputs list, matching the order of submit_quiz_outputs outputs = [] # First, handle the common UI elements: outputs.append(gr.update(visible=False)) # submit_quiz_button (will be hidden) outputs.append(gr.update(value="", visible=False)) # quiz_results_display (cleared and hidden, will be updated) outputs.append(gr.update(visible=True)) # start_new_quiz_button (will be updated) - Always show this after submit outputs.append(gr.update(visible=False)) # interactive_quiz_column (will be hidden) # Initialize updates for all quiz question and radio button components # These must be included in the outputs tuple, even if they are hidden for _ in range(MAX_QUIZ_QUESTIONS_UI): outputs.append(gr.update(value="", visible=False)) # quiz_question_md[i] - No 'choices' here outputs.append(gr.update(value=None, visible=False, choices=[])) # quiz_options_radio[i] - important to clear choices too # --- Now check if the list retrieved from quiz_context is empty --- if not parsed_quiz_data: # Check if the list 'parsed_quiz_data' is empty print("DEBUG: parsed_quiz_data (from quiz_context) is empty. Returning 'No quiz data found' message.") outputs[1] = gr.update(value="No quiz data found. Please generate a quiz first.", visible=True) outputs[2] = gr.update(visible=True) # Show start new quiz button return tuple(outputs) score = 0 feedback_markdown = "## Quiz Results\n\n" for i, question in enumerate(parsed_quiz_data): # Iterate directly over the list user_answer_display = "No answer provided" user_choice_letter = None if i < len(user_answers_raw) and user_answers_raw[i] is not None: user_answer_display = user_answers_raw[i] # Extract just the letter (A, B, C, D) from user_answer_raw (e.g., "A. Option Text") if isinstance(user_answers_raw[i], str) and len(user_answers_raw[i]) >= 1: user_choice_letter = user_answers_raw[i][0] correct_answer_letter = question['correct_answer'] is_correct = (user_choice_letter == correct_answer_letter) if is_correct: score += 1 feedback_markdown += f"ā **Question {i+1}: Correct!**\n" else: feedback_markdown += f"ā **Question {i+1}: Incorrect.**\n" feedback_markdown += f"**Your Answer:** {user_answer_display}\n" feedback_markdown += f"**Correct Answer:** {correct_answer_letter}. {question['options'].get(correct_answer_letter, 'Option not found')}\n" feedback_markdown += f"**Explanation:** {question['explanation']}\n\n---\n\n" total_questions = len(parsed_quiz_data) # Get length directly from the list final_score_text = f"## Your Score: {score} out of {total_questions}\n\n" feedback_markdown = final_score_text + feedback_markdown # Record result to database - ensure score and total_questions are integers record_quiz_result(quiz_context["topic"], quiz_context["difficulty"], int(score), int(total_questions)) # Update the relevant output components in the 'outputs' list outputs[0] = gr.update(visible=False) # submit_quiz_button outputs[1] = gr.update(value=feedback_markdown, visible=True) # quiz_results_display outputs[2] = gr.update(visible=True) # start_new_quiz_button outputs[3] = gr.update(visible=False) # interactive_quiz_column (hide this after submission) # All other question/radio updates are already set to hidden/cleared by the initial `outputs` list setup. return tuple(outputs) def start_new_quiz_ui_reset(): """Resets the quiz UI to its initial state, hiding quiz and results elements.""" outputs = [ gr.update(value="", visible=True), # quiz_topic_input gr.update(value="3", visible=True), # num_questions_input gr.update(value="medium", visible=True), # quiz_difficulty gr.update(value=None, visible=True), # pdf_upload_input gr.update(visible=True), # generate_quiz_button gr.update(value="", visible=False), # raw_quiz_output_display (ensure hidden, now a Textbox) gr.update(visible=False), # submit_quiz_button (hidden) gr.update(value="", visible=False), # quiz_results_display (hidden) gr.update(visible=False), # start_new_quiz_button (hidden) gr.update(visible=False) # interactive_quiz_column (hide this too) ] # Hide all question fields and reset their values for i in range(MAX_QUIZ_QUESTIONS_UI): outputs.extend([ gr.update(value="", visible=False), # quiz_question_md[i] - No 'choices' here gr.update(value=None, visible=False, choices=[]) # quiz_options_radio[i] - important to clear choices too ]) return tuple(outputs) # --- Summarizer, Refiner, Word, Translate Functions (Unchanged - ensure they use gr.update for outputs) --- def summarize_text(input_text, summary_length="short"): if model is None or tokenizer is None: yield gr.update(value="Error: Model not loaded. Please check Colab runtime and GPU memory.") return if not input_text: yield gr.update(value="Please provide text to summarize.") return if summary_length == "short": prompt_instruction = "Summarize the following text concisely:" max_output_tokens = 100 elif summary_length == "detailed": prompt_instruction = "Provide a detailed summary of the following text:" max_output_tokens = 250 else: # simple_explanation prompt_instruction = "Explain the following text in simple terms, suitable for a beginner:" max_output_tokens = 200 summary_prompt = f"{prompt_instruction}\n\n{input_text}" messages = [{"role": "user", "content": summary_prompt}] input_text_tokenized = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) summary_input_ids = tokenizer(input_text_tokenized, return_tensors="pt").to(device) current_summary = "" try: full_output_tokens = model.generate( **summary_input_ids, max_new_tokens=max_output_tokens, temperature=0.7, do_sample=True, pad_token_id=tokenizer.eos_token_id ) summary_raw = tokenizer.decode(full_output_tokens[0][summary_input_ids.input_ids.shape[1]:], skip_special_tokens=True) words = summary_raw.split(" ") for i in range(len(words)): current_summary += words[i] + " " yield gr.update(value=current_summary) # Ensure outputs are gr.update time.sleep(STREAM_DELAY) return except Exception as e: yield gr.update(value=f"Error during summarization: {e}") # Ensure outputs are gr.update def refine_text(input_text): if model is None or tokenizer is None: yield gr.update(value="Error: Model not loaded. Please check Colab runtime and GPU memory.") return if not input_text: yield gr.update(value="Please provide text to refine.") return refine_prompt = f"""Review the following text for grammar, spelling, punctuation, and clarity. Provide corrected sentences and suggest improvements for style and conciseness. Explain any major changes. Original Text: {input_text} Refined Text: """ messages = [{"role": "user", "content": refine_prompt}] input_text_tokenized = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) refine_input_ids = tokenizer(input_text_tokenized, return_tensors="pt").to(device) current_refined_text = "" try: full_output_tokens = model.generate( **refine_input_ids, max_new_tokens=400, temperature=0.7, do_sample=True, pad_token_id=tokenizer.eos_token_id ) refined_text_raw = tokenizer.decode(full_output_tokens[0][refine_input_ids.input_ids.shape[1]:], skip_special_tokens=True) words = refined_text_raw.split(" ") for i in range(len(words)): current_refined_text += words[i] + " " yield gr.update(value=current_refined_text) # Ensure outputs are gr.update time.sleep(STREAM_DELAY) return except Exception as e: yield gr.update(value=f"Error refining text: {e}") # Ensure outputs are gr.update # Function for Word Meaning & Usage def get_word_meaning_and_usage(word): if model is None or tokenizer is None: yield gr.update(value="Error: Model not loaded.") return if not word: yield gr.update(value="Please enter a word.") return prompt = f"""Provide the meaning of the word '{word}' and demonstrate its usage in two different example sentences. Format your response clearly. Word: {word} Meaning: Example 1: Example 2: """ messages = [{"role": "user", "content": prompt}] input_text_tokenized = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) input_ids = tokenizer(input_text_tokenized, return_tensors="pt").to(device) current_output = "" try: full_output_tokens = model.generate( **input_ids, max_new_tokens=150, temperature=0.7, do_sample=True, pad_token_id=tokenizer.eos_token_id ) raw_output = tokenizer.decode(full_output_tokens[0][input_ids.input_ids.shape[1]:], skip_special_tokens=True) words = raw_output.split(" ") for i in range(len(words)): current_output += words[i] + " " yield gr.update(value=current_output) # Ensure outputs are gr.update time.sleep(STREAM_DELAY) return except Exception as e: yield gr.update(value=f"Error getting word meaning: {e}") # Ensure outputs are gr.update # Function for Sentence Translation def translate_sentence(sentence, target_language="Hindi"): if model is None or tokenizer is None: yield gr.update(value="Error: Model not loaded.") return if not sentence: yield gr.update(value="Please enter a sentence to translate.") return prompt = f"Translate the following English sentence into {target_language}:\n\nEnglish: {sentence}\n{target_language}:" messages = [{"role": "user", "content": prompt}] input_text_tokenized = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) input_ids = tokenizer(input_text_tokenized, return_tensors="pt").to(device) current_output = "" try: full_output_tokens = model.generate( **input_ids, max_new_tokens=100, temperature=0.7, do_sample=True, pad_token_id=tokenizer.eos_token_id ) raw_output = tokenizer.decode(full_output_tokens[0][input_ids.input_ids.shape[1]:], skip_special_tokens=True) words = raw_output.split(" ") for i in range(len(words)): current_output += words[i] + " " yield gr.update(value=current_output) # Ensure outputs are gr.update time.sleep(STREAM_DELAY) return except Exception as e: yield gr.update(value=f"Error translating sentence: {e}") # Ensure outputs are gr.update # Initialize database init_db() # Define the chat functions OUTSIDE the Blocks for proper scoping def user_message(user_message, history): # This now yields a gr.update to set the textbox value to empty # and updates the chatbot history to show the user's message immediately. return gr.update(value=""), history + [[user_message, None]] def bot_response(history): if not history: # Yield an update for the chatbot to display an error yield gr.update(value=[["", "Error: Chat history is empty."]]) return user_message_text = history[-1][0] # Generate the full response using the helper function full_bot_response = "" for chunk in generate_response(user_message_text, history[:-1]): full_bot_response = chunk history[-1][1] = full_bot_response yield history def clear_chat(): return None, None # Make sure quiz_context = {} is defined globally at the top of your script, e.g.: # quiz_context = {} # --- Gradio UI --- with gr.Blocks(title="Edu-Tutor AI", theme=gr.themes.Soft()) as demo: gr.Markdown("#