# player_interface.py import gradio as gr import pandas as pd from quiz_manager import quiz_manager # Assuming quiz_manager exists and is accessible class PlayerInterface: def __init__(self): self.current_game_pin = None self.current_question_index = -1 self.current_quiz_data = None self.player_name = None self.total_questions = 0 # Modified join_game to return updates for the Group visibility def join_game(self, game_pin, player_name): """Validate the game pin and allow player to join.""" print(f"Attempting to join game: Pin={game_pin}, Player={player_name}") if not game_pin or not game_pin.strip() or not player_name or not player_name.strip(): print("JOIN_GAME: Empty pin or name") # Return outputs for staying on the join screen return ( "Please enter both Game Pin and your Name.", # Join Status gr.update(visible=True), # Show Join Group gr.update(visible=False), # Hide Game Area gr.update(visible=False), # Hide Leaderboard Area gr.update(choices=[], visible=False, value=None), # Hide Question Dropdown gr.update(visible=False), # Hide Next Button Row gr.update(value="") # Clear Question Display ) try: # Check if quiz exists quiz = quiz_manager.get_quiz(game_pin.strip()) if not quiz: print(f"JOIN_GAME: Invalid pin: {game_pin}") # Return outputs for staying on the join screen with error return ( f"Invalid game pin: {game_pin.strip()}. Please try again.", gr.update(visible=True), # Show Join Group gr.update(visible=False), # Hide Game Area gr.update(visible=False), # Hide Leaderboard Area gr.update(choices=[], visible=False, value=None), # Hide Question Dropdown gr.update(visible=False), # Hide Next Button Row gr.update(value="") # Clear Question Display ) self.current_game_pin = game_pin.strip() self.player_name = player_name.strip() self.current_quiz_data = quiz_manager.quizzes.get(self.current_game_pin) # Initialize player score if they are new to this quiz quiz_manager.initialize_player_score(self.current_game_pin, self.player_name) self.total_questions = len(self.current_quiz_data.get('questions', [])) question_numbers = list(range(1, self.total_questions + 1)) print(f"JOIN_GAME: Success for {self.player_name} in {self.current_game_pin}. Total questions: {self.total_questions}") # Return updates to hide Join Group, show Game Area, show Leaderboard Area, and update dropdown return ( f"Joined game {self.current_game_pin} successfully, {self.player_name}!", # Join Status gr.update(visible=False), # Hide Join Group gr.update(visible=True), # Show Game Area gr.update(visible=True), # Show Leaderboard Area # Update choices, make visible if questions exist, reset value gr.update(choices=question_numbers, visible=self.total_questions > 0, value=None), gr.update(visible=self.total_questions > 0), # Show Next Button Row only if questions exist gr.update(value="Click 'Start Quiz' to begin.") # Set initial text for Question Display ) except Exception as e: print(f"JOIN_GAME: Error - {str(e)}") # Return updates for all relevant components on error, stay on join screen return ( f"Error joining game: {str(e)}", gr.update(visible=True), # Show Join Group gr.update(visible=False), # Hide Game Area gr.update(visible=False), # Hide Leaderboard Area gr.update(choices=[], visible=False, value=None), # Hide Question Dropdown gr.update(visible=False), # Hide Next Button Row gr.update(value="") # Clear Question Display ) def show_question(self): """Prepare updates for displaying the current question.""" print("SHOW_QUESTION: Called") # Prepare default updates for components default_options_update = gr.update(choices=[], value=None, visible=False) default_prompt_label_update = gr.update(value="", visible=False) default_question_area_update = gr.update(visible=True) # Assume area itself is visible when showing a question default_submit_btn_update = gr.update(visible=True) default_answer_output_update = gr.update(value="") if not self.current_quiz_data or self.current_question_index == -1: print("SHOW_QUESTION: No quiz data or invalid index") return ( "Error: Could not load question.", default_options_update, default_prompt_label_update, gr.update(visible=False), # Hide question area if no data gr.update(visible=False), # Hide submit button if no data gr.update(value="") ) questions = self.current_quiz_data.get('questions', []) if not questions or self.current_question_index < 0 or self.current_question_index >= len(questions): print("SHOW_QUESTION: Invalid question data or index out of bounds") return ( "Error loading question data.", default_options_update, default_prompt_label_update, gr.update(visible=False), gr.update(visible=False), gr.update(value="") ) question_data = questions[self.current_question_index] options = question_data.get('options', []) question_text = question_data.get('text', 'Error loading question text') print(f"SHOW_QUESTION: Displaying question - index: {self.current_question_index}, text: {question_text}") # Return specific updates for the current question return ( f"**Question {self.current_question_index + 1}/{self.total_questions}:** {question_text}", # Update question_display value with bolding gr.update(choices=options, value=None, label="Answer Choices", visible=True), # Update options_radio, make visible gr.update(value="Select your answer:", visible=True), # Update prompt label, make visible gr.update(visible=True), # Make question_area visible gr.update(visible=True), # Make submit_btn visible gr.update(value="") # Clear answer_output value ) # Modified methods to return updates for dropdown and next button row visibility # next_step returns 8 outputs def next_step(self): """Starts the quiz, showing the first question.""" print("NEXT_STEP: Called (Start Quiz)") if self.current_quiz_data and self.total_questions > 0: self.current_question_index = 0 question_updates = self.show_question() # 6 outputs print(f"NEXT_STEP: Showing question - index: {self.current_question_index}") return ( *question_updates, # Unpack the 6 updates gr.update(visible=False), # Hide Next Button Row gr.update(value=self.current_question_index + 1) # Update dropdown value ) else: print("NEXT_STEP: No questions available") # Return default updates if no questions, hide game elements return ( "No questions available for this quiz.", # Question Display (1) gr.update(choices=[], value=None, label="Answer Choices", visible=False), # Options Radio (2) gr.update(value="", visible=False), # Prompt Label (3) gr.update(visible=False), # Question Area (4) gr.update(visible=False), # Submit Button (5) gr.update(value=""), # Answer Output (6) gr.update(visible=False), # Hide Next Button Row (7) gr.update(value=None) # Clear dropdown value (8) ) # next_question returns 8 outputs def next_question(self): """Move to the next question.""" print("NEXT_QUESTION: Called") if not self.current_quiz_data or not self.current_game_pin or not self.player_name: print("NEXT_QUESTION: No quiz data or player context") return ( "Please join a game first.", # Question Display (1) gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) gr.update(value="", visible=False), # (3) gr.update(visible=False), # (4) gr.update(visible=False), # (5) gr.update(value=""), # (6) gr.update(visible=True), # Keep Next Button Row visible if no quiz data (7) gr.update(value=None) # Clear dropdown value (8) ) questions = self.current_quiz_data.get('questions', []) if self.current_question_index < len(questions) - 1: self.current_question_index += 1 question_updates = self.show_question() # 6 outputs print(f"NEXT_QUESTION: Showing question - index: {self.current_question_index}") return ( *question_updates, gr.update(visible=False), # Keep Next Button Row hidden (7) gr.update(value=self.current_question_index + 1) # Update dropdown value (8) ) else: print("NEXT_QUESTION: Quiz finished") # Reset instance variables self.current_question_index = -1 self.current_quiz_data = None self.current_game_pin = None self.player_name = None self.total_questions = 0 # Return updates for quiz finished state, hide game elements, show join box needs separate trigger return ( "Quiz finished! You can join another game.", # Question Display (1) gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) gr.update(value="", visible=False), # (3) gr.update(visible=False), # (4) gr.update(visible=False), # (5) gr.update(value=""), # (6) gr.update(visible=False), # Hide Next Button Row (7) gr.update(value=None) # Clear dropdown value (8) # Note: Join Box, Game Area, Leaderboard Area visibility needs to be handled by a separate action or logic # A 'Play Again' or 'Join New Game' button in the finished state might be better UI. ) # previous_question returns 8 outputs def previous_question(self): """Move to the previous question.""" print("PREVIOUS_QUESTION: Called") if not self.current_quiz_data or not self.current_game_pin or not self.player_name: print("PREVIOUS_QUESTION: No quiz data or player context") return ( "Please join a game first.", # Question Display (1) gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) gr.update(value="", visible=False), # (3) gr.update(visible=False), # (4) gr.update(visible=False), # (5) gr.update(value=""), # (6) gr.update(visible=True), # Keep Next Button Row visible if no quiz data (7) gr.update(value=None) # Clear dropdown value (8) ) if self.current_question_index > 0: self.current_question_index -= 1 question_updates = self.show_question() # 6 outputs print(f"PREVIOUS_QUESTION: Showing question - index: {self.current_question_index}") return ( *question_updates, gr.update(visible=False), # Keep Next Button Row hidden (7) gr.update(value=self.current_question_index + 1) # Update dropdown value (8) ) else: print("PREVIOUS_QUESTION: First question") # show_question already sets visibility for elements in question_area question_updates = self.show_question() # 6 outputs for current question (index 0) return ( *question_updates, # Includes showing question area elements gr.update(visible=False), # Keep Next Button Row hidden (7) gr.update(value=self.current_question_index + 1) # Keep dropdown value at 1 (8) ) # select_question_by_number returns 8 outputs def select_question_by_number(self, question_number): """Select a specific question by its number.""" print(f"SELECT_QUESTION_BY_NUMBER: Called with {question_number}") # Handle case where question_number might be None from dropdown reset or initial state if question_number is None: print("SELECT_QUESTION_BY_NUMBER: question_number is None") # Return outputs to clear question area, hide game elements return ( "", # Clear question text (1) gr.update(choices=[], value=None, label="Answer Choices", visible=False), # Clear options (2) gr.update(value="", visible=False), # Clear prompt label (3) gr.update(visible=False), # Hide question area (4) gr.update(visible=False), # Hide submit button (5) gr.update(value=""), # Clear answer output (6) gr.update(visible=False), # Hide Next Button Row (7) gr.update(value=None) # Clear dropdown value (8) ) if not self.current_quiz_data or not self.current_game_pin or not self.player_name: print("SELECT_QUESTION_BY_NUMBER: No quiz data or player context") # Return default updates, hide game elements return ( "Please join a game first.", # (1) gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) gr.update(value="", visible=False), # (3) gr.update(visible=False), # (4) gr.update(visible=False), # (5) gr.update(value=""), # (6) gr.update(visible=False), # (7) gr.update(value=None) # (8) ) questions = self.current_quiz_data.get('questions', []) # Convert question_number to integer index try: index = int(question_number) - 1 except (ValueError, TypeError): print("SELECT_QUESTION_BY_NUMBER: Invalid number input") return ( "Invalid input for question number.", # (1) gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) gr.update(value="", visible=False), # (3) gr.update(visible=False), # (4) gr.update(visible=False), # (5) gr.update(value=""), # (6) gr.update(visible=False), # (7) gr.update(value=None) # (8) ) if 0 <= index < len(questions): self.current_question_index = index question_updates = self.show_question() # 6 outputs print(f"SELECT_QUESTION_BY_NUMBER: Showing question - index: {self.current_question_index}") # Append updates for the dropdown (keep value) and keep next button row hidden return ( *question_updates, gr.update(visible=False), # Keep Next Button Row hidden (7) gr.update(value=self.current_question_index + 1) # Keep dropdown value (8) ) else: print("SELECT_QUESTION_BY_NUMBER: Invalid question number") # Return updates for invalid number, hide game elements return ( "Invalid question number.", # (1) gr.update(choices=[], value=None, label="Answer Choices", visible=False), # (2) gr.update(value="", visible=False), # (3) gr.update(visible=False), # (4) gr.update(visible=False), # (5) gr.update(value=""), # (6) gr.update(visible=False), # (7) gr.update(value=None) # (8) ) def submit_answer(self, selected_option): """Submit the player's answer for the current question.""" if not self.current_quiz_data or self.current_question_index == -1 or not self.current_game_pin or not self.player_name: return "Please join a game and select a question first." # Ensure selected_option is not None if the user submits without selecting if selected_option is None: return "Please select an answer." # Fetch quiz data again to ensure it's fresh before checking answer quiz = quiz_manager.get_quiz(self.current_game_pin) if not quiz or 'questions' not in quiz or self.current_question_index < 0 or self.current_question_index >= len(quiz['questions']): return "Error submitting answer: Could not retrieve current question data." current_question = quiz['questions'][self.current_question_index] correct_answer = current_question.get('correct_answer') # Handle cases where correct_answer might be missing if correct_answer is None: return "Error submitting answer: Correct answer not defined for this question." is_correct = selected_option.strip() == correct_answer.strip() try: quiz_manager.record_answer(self.current_game_pin, self.player_name, is_correct) except Exception as e: return f"Error recording score: {str(e)}" return "Correct!" if is_correct else f"Incorrect. The correct answer was: {correct_answer}" def get_leaderboard(self): """Retrieve and display the leaderboard.""" print("GET_LEADERBOARD: Called") # This method is called by the Refresh button *inside* the leaderboard_area. # It relies on self.current_game_pin being set by the join_game method. if not self.current_game_pin: # This case should ideally not happen if UI flow is correct, but for safety return "Please join a game first to view the leaderboard." try: leaderboard = quiz_manager.get_leaderboard(self.current_game_pin) if not leaderboard: print("GET_LEADERBOARD: No scores recorded") return "No scores recorded yet." leaderboard_str = "### Leaderboard:\n" # Sort leaderboard by score in descending order try: # get_leaderboard in quiz_manager returns a sorted list of (name, data) tuples sorted_leaderboard = leaderboard # It's already sorted by quiz_manager for i, (name, data) in enumerate(sorted_leaderboard, 1): score = data.get('score', 0) # Safely get score from the dict leaderboard_str += f"{i}. {name}: {score} points\n" print("GET_LEADERBOARD: Leaderboard generated") return leaderboard_str except (TypeError, IndexError, AttributeError) as e: # Catch potential errors if the list elements are not in the expected format print(f"GET_LEADERBOARD: Error processing leaderboard data - {str(e)}") # Display raw data for debugging return f"Error displaying leaderboard data: {str(e)}.\nRaw data: {leaderboard}" except Exception as e: return f"Error retrieving leaderboard: {str(e)}" def create_player_interface(self): """Create the Gradio interface for players with a step-by-step layout.""" with gr.Blocks() as player_interface: gr.Markdown("## Quiz Player Dashboard") # --- Define all Components First --- # Join Game Section (Visible initially) # Replaced gr.Box with gr.Group join_group = gr.Group(visible=True) with join_group: gr.Markdown("### Join Game") with gr.Row(): game_pin_input = gr.Textbox(label="Enter Game Pin", scale=1) player_name_input = gr.Textbox(label="Your Name", scale=1) join_btn = gr.Button("Join Game", scale=1) join_output = gr.Textbox(label="Join Status", interactive=False) # Game Area (Hidden initially, shown after joining) game_area = gr.Column(visible=False) with game_area: gr.Markdown("### Play Quiz") # Initial Next Button (to start the quiz) - part of game_area but controlled separately next_button_row = gr.Row(visible=False) # Controlled visible/hidden with next_button_row: next_btn = gr.Button("Start Quiz") # Question Dropdown (visibility controlled dynamically) question_dropdown = gr.Dropdown(label="Select Question", choices=[], visible=False, interactive=True) # Question Display Area (Hidden initially, shown after starting/selecting question) question_area = gr.Column(visible=False) # Controlled visible/hidden with question_area: question_display = gr.Markdown(label="Question Text") options_prompt_label = gr.Label(label="", visible=False) options_radio = gr.Radio(label="Answer Choices", visible=False) submit_btn = gr.Button("Submit Answer", visible=False) answer_output = gr.Textbox(label="Answer Result", interactive=False) # Navigation Buttons with gr.Row(): prev_question_btn = gr.Button("Previous Question") next_question_btn = gr.Button("Next Question") # Leaderboard Area (Hidden initially, shown after joining) leaderboard_area = gr.Column(visible=False) # Controlled visible/hidden with leaderboard_area: gr.Markdown("### Current Leaderboard") leaderboard_output = gr.Markdown(label="Leaderboard Results") get_leaderboard_btn = gr.Button("Refresh Leaderboard") # --- Define all Interactions After Components --- # Join Game interaction # join_game returns 7 outputs: status, join_group_vis, game_area_vis, leaderboard_area_vis, dropdown_update, next_btn_row_vis, question_display_initial_text join_btn.click( fn=self.join_game, inputs=[game_pin_input, player_name_input], outputs=[ join_output, join_group, # Update visibility of join group game_area, # Update visibility of game area leaderboard_area, # Update visibility of leaderboard area question_dropdown, # Update dropdown choices/visibility next_button_row, # Update visibility of next button row question_display # Update initial text of question display ] ) # Start Quiz button interaction (uses next_step to show the first question) # next_step returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value next_btn.click( fn=self.next_step, inputs=None, # next_step uses instance variables outputs=[ question_display, options_radio, options_prompt_label, question_area, submit_btn, answer_output, next_button_row, question_dropdown # Update dropdown value ] ) # Question Dropdown interaction # select_question_by_number returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value question_dropdown.change( fn=self.select_question_by_number, inputs=question_dropdown, # Pass the selected value (the question number) outputs=[ question_display, options_radio, options_prompt_label, question_area, submit_btn, answer_output, next_button_row, # Ensure next button row is hidden after selection question_dropdown # Keep dropdown value updated ] ) # Next Question button interaction # next_question returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value next_question_btn.click( fn=self.next_question, inputs=None, # Uses instance variables outputs=[ question_display, options_radio, options_prompt_label, question_area, submit_btn, answer_output, next_button_row, question_dropdown # Update dropdown value ] ) # Previous Question button interaction # previous_question returns 8 outputs: question_display, options_radio, prompt_label, question_area, submit_btn, answer_output, next_btn_row_vis, dropdown_value prev_question_btn.click( fn=self.previous_question, inputs=None, # Uses instance variables outputs=[ question_display, options_radio, options_prompt_label, question_area, submit_btn, answer_output, next_button_row, question_dropdown # Update dropdown value ] ) # Submit Answer button interaction submit_btn.click( fn=self.submit_answer, inputs=options_radio, # Input is the selected value from the radio button outputs=answer_output, # Output is the result of the submission ) # Refresh Leaderboard button interaction get_leaderboard_btn.click( fn=self.get_leaderboard, inputs=None, # Uses instance variables outputs=leaderboard_output, # Output is the formatted leaderboard string ) # No specific load interaction needed here, join_game handles initial visibility. return player_interface # --- How to use this class --- # Create the PlayerInterface instance player_interface_instance = PlayerInterface() # To access the Gradio interface object: # The app.py file will call this method to get the interface block player_gradio_interface = player_interface_instance.create_player_interface() # You can now use player_gradio_interface in your main app.py file.