Spaces:
Sleeping
Sleeping
| """ | |
| UI Components Module | |
| Defines Gradio interface CSS styles and UI component building functions. | |
| """ | |
| # ==================== Custom CSS Styles ==================== | |
| CUSTOM_CSS = """ | |
| /* Global styles - Minimalist white UI, black text, no emojis */ | |
| .gradio-container { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif; | |
| background-color: white; | |
| color: black; | |
| } | |
| /* Main container background - Dynamic color controlled by Python */ | |
| .main-container { | |
| transition: background-color 0.5s ease; | |
| padding: 20px; | |
| border-radius: 8px; | |
| } | |
| /* Button styles */ | |
| .primary-button { | |
| background-color: black !important; | |
| color: white !important; | |
| border: none !important; | |
| padding: 10px 20px !important; | |
| font-size: 16px !important; | |
| font-weight: 500 !important; | |
| cursor: pointer !important; | |
| transition: opacity 0.2s ease !important; | |
| border-radius: 4px !important; | |
| } | |
| .primary-button:hover { | |
| opacity: 0.8 !important; | |
| } | |
| .secondary-button { | |
| background-color: white !important; | |
| color: black !important; | |
| border: 2px solid black !important; | |
| padding: 10px 20px !important; | |
| font-size: 16px !important; | |
| font-weight: 500 !important; | |
| cursor: pointer !important; | |
| transition: background-color 0.2s ease !important; | |
| border-radius: 4px !important; | |
| } | |
| .secondary-button:hover { | |
| background-color: #f0f0f0 !important; | |
| } | |
| /* Image display area */ | |
| .image-display { | |
| border: 2px solid black; | |
| border-radius: 8px; | |
| padding: 10px; | |
| background-color: white; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .image-display img { | |
| max-width: 100%; | |
| max-height: 600px; | |
| object-fit: contain; | |
| } | |
| /* Score display */ | |
| .score-display { | |
| font-size: 24px; | |
| font-weight: 600; | |
| text-align: center; | |
| padding: 15px; | |
| background-color: white; | |
| border: 2px solid black; | |
| border-radius: 8px; | |
| margin: 10px 0; | |
| } | |
| /* Progress display */ | |
| .progress-display { | |
| font-size: 18px; | |
| font-weight: 500; | |
| text-align: center; | |
| padding: 10px; | |
| background-color: white; | |
| border: 1px solid black; | |
| border-radius: 4px; | |
| margin: 10px 0; | |
| } | |
| /* Feedback message */ | |
| .feedback-box { | |
| font-size: 16px; | |
| padding: 15px; | |
| background-color: white; | |
| border: 2px solid black; | |
| border-radius: 8px; | |
| margin: 10px 0; | |
| min-height: 60px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| } | |
| /* Mode selector area */ | |
| .mode-selector { | |
| display: flex; | |
| justify-content: center; | |
| gap: 20px; | |
| margin: 20px 0; | |
| } | |
| /* Sidebar styles */ | |
| .sidebar { | |
| background-color: white; | |
| border-right: 2px solid black; | |
| padding: 20px; | |
| min-height: 100vh; | |
| } | |
| /* Main content area */ | |
| .main-content { | |
| padding: 20px; | |
| flex: 1; | |
| } | |
| /* Markdown styles */ | |
| .markdown-text { | |
| color: black; | |
| font-size: 16px; | |
| line-height: 1.6; | |
| } | |
| /* Heading styles */ | |
| h1, h2, h3, h4, h5, h6 { | |
| color: black; | |
| font-weight: 600; | |
| margin: 10px 0; | |
| } | |
| /* Hide unnecessary Gradio elements */ | |
| .footer { | |
| display: none !important; | |
| } | |
| /* Detector mode specific styles */ | |
| .detector-result { | |
| font-size: 20px; | |
| font-weight: 600; | |
| padding: 20px; | |
| background-color: white; | |
| border: 2px solid black; | |
| border-radius: 8px; | |
| text-align: center; | |
| } | |
| .confidence-display { | |
| font-size: 18px; | |
| padding: 15px; | |
| background-color: white; | |
| border: 1px solid black; | |
| border-radius: 8px; | |
| margin-top: 10px; | |
| } | |
| /* Game over styles */ | |
| .game-over-banner { | |
| font-size: 28px; | |
| font-weight: 700; | |
| text-align: center; | |
| padding: 30px; | |
| background-color: white; | |
| border: 3px solid black; | |
| border-radius: 12px; | |
| margin: 20px 0; | |
| } | |
| /* Responsive design */ | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| border-right: none; | |
| border-bottom: 2px solid black; | |
| min-height: auto; | |
| } | |
| .score-display { | |
| font-size: 20px; | |
| } | |
| .progress-display { | |
| font-size: 16px; | |
| } | |
| .feedback-box { | |
| font-size: 14px; | |
| } | |
| } | |
| /* Disabled state styles */ | |
| .disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed !important; | |
| pointer-events: none; | |
| } | |
| /* Loading state */ | |
| .loading { | |
| opacity: 0.6; | |
| pointer-events: none; | |
| } | |
| /* Success/error indicators */ | |
| .success-indicator { | |
| color: #2e7d32; | |
| font-weight: 600; | |
| } | |
| .error-indicator { | |
| color: #c62828; | |
| font-weight: 600; | |
| } | |
| /* Divider */ | |
| .divider { | |
| border-top: 1px solid #000; | |
| margin: 20px 0; | |
| } | |
| """ | |
| # ==================== UI Component Helper Functions ==================== | |
| def get_custom_css() -> str: | |
| """ | |
| Return custom CSS style string | |
| Returns: | |
| CSS style string | |
| """ | |
| return CUSTOM_CSS | |
| def create_score_html(player_score: int, ai_score: int) -> str: | |
| """ | |
| Create HTML for score display | |
| Args: | |
| player_score: Player score | |
| ai_score: AI score | |
| Returns: | |
| HTML string | |
| """ | |
| return f""" | |
| <div class="score-display"> | |
| You: {player_score} | AI: {ai_score} | |
| </div> | |
| """ | |
| def create_progress_html(current_round: int, total_rounds: int) -> str: | |
| """ | |
| Create HTML for progress display | |
| Args: | |
| current_round: Current round | |
| total_rounds: Total rounds | |
| Returns: | |
| HTML string | |
| """ | |
| return f""" | |
| <div class="progress-display"> | |
| Round: {current_round}/{total_rounds} | |
| </div> | |
| """ | |
| def create_feedback_html(message: str, is_error: bool = False) -> str: | |
| """ | |
| Create HTML for feedback message | |
| Args: | |
| message: Feedback message | |
| is_error: Whether this is an error message | |
| Returns: | |
| HTML string | |
| """ | |
| css_class = "error-indicator" if is_error else "" | |
| return f""" | |
| <div class="feedback-box"> | |
| <span class="{css_class}">{message}</span> | |
| </div> | |
| """ | |
| def create_detector_result_html(prediction: str, confidence: float) -> str: | |
| """ | |
| Create HTML for detector result | |
| Args: | |
| prediction: Prediction result ("AI" or "Human") | |
| confidence: Confidence score | |
| Returns: | |
| HTML string | |
| """ | |
| return f""" | |
| <div class="detector-result"> | |
| Prediction: {prediction} | |
| </div> | |
| <div class="confidence-display"> | |
| Confidence: {confidence * 100:.1f}% | |
| </div> | |
| """ | |
| def create_game_over_html(player_score: int, ai_score: int, total_rounds: int) -> str: | |
| """ | |
| Create HTML for game over screen | |
| Args: | |
| player_score: Player score | |
| ai_score: AI score | |
| total_rounds: Total rounds | |
| Returns: | |
| HTML string | |
| """ | |
| # Determine winner | |
| if player_score > ai_score: | |
| result_text = "You won!" | |
| elif player_score < ai_score: | |
| result_text = "AI won!" | |
| else: | |
| result_text = "It's a tie!" | |
| player_pct = (player_score / total_rounds) * 100 | |
| ai_pct = (ai_score / total_rounds) * 100 | |
| return f""" | |
| <div class="game-over-banner"> | |
| {result_text} | |
| </div> | |
| <div class="score-display"> | |
| Your score: {player_score}/{total_rounds} ({player_pct:.1f}%)<br> | |
| AI score: {ai_score}/{total_rounds} ({ai_pct:.1f}%) | |
| </div> | |
| """ | |
| # ==================== UI Interface Building Functions ==================== | |
| def format_score(player_score: int, ai_score: int, current_round: int, total_rounds: int) -> str: | |
| """ | |
| Format score display as Markdown text | |
| Args: | |
| player_score: Player score | |
| ai_score: AI score | |
| current_round: Current round | |
| total_rounds: Total rounds | |
| Returns: | |
| Markdown formatted score text | |
| """ | |
| return f"""### Score | |
| **You**: {player_score} | |
| **AI**: {ai_score} | |
| **Progress**: {current_round}/{total_rounds} | |
| """ | |
| def create_game_interface(): | |
| """ | |
| Create game mode UI interface (using Gradio components) | |
| Returns: | |
| Dictionary containing all UI components | |
| """ | |
| import gradio as gr | |
| components = {} | |
| with gr.Row(): | |
| # Left sidebar (1:3 ratio) | |
| with gr.Column(scale=1): | |
| components['score_display'] = gr.Markdown( | |
| value=format_score(0, 0, 0, 20), | |
| elem_classes=["markdown-text"] | |
| ) | |
| components['ai_button'] = gr.Button( | |
| "AI Generated", | |
| variant="primary", | |
| size="lg", | |
| elem_classes=["primary-button"] | |
| ) | |
| components['human_button'] = gr.Button( | |
| "Real Human", | |
| variant="secondary", | |
| size="lg", | |
| elem_classes=["secondary-button"] | |
| ) | |
| components['next_button'] = gr.Button( | |
| "Next", | |
| size="lg", | |
| elem_classes=["secondary-button"], | |
| visible=False | |
| ) | |
| components['reset_button'] = gr.Button( | |
| "Reset Game", | |
| size="sm", | |
| elem_classes=["secondary-button"] | |
| ) | |
| components['feedback_display'] = gr.Markdown( | |
| value="", | |
| elem_classes=["markdown-text", "feedback-box"] | |
| ) | |
| # Right main content area (3:1 ratio) | |
| with gr.Column(scale=3): | |
| components['image_display'] = gr.Image( | |
| label="Face Image", | |
| type="filepath", | |
| interactive=False, | |
| elem_classes=["image-display"] | |
| ) | |
| return components | |
| def handle_guess(player_guess: str, state, image_pool, model): | |
| """ | |
| Handle player's guess | |
| Args: | |
| player_guess: Player's guess ("AI" or "Human") | |
| state: GameState instance | |
| image_pool: ImagePool instance | |
| model: FaceDetectorModel instance | |
| Returns: | |
| Updated UI component values (score, feedback, button states, etc.) | |
| """ | |
| # If already guessed or game over, don't process | |
| if state.guess_made or state.game_over: | |
| return {} | |
| # Get AI prediction | |
| ai_prediction, ai_confidence = model.predict(state.current_image_path) | |
| # Record guess result | |
| state.record_guess(player_guess, ai_prediction, ai_confidence) | |
| # Prepare feedback | |
| feedback = state.last_result | |
| # Check if game is over | |
| if state.game_over: | |
| feedback = state.get_final_result() | |
| # Return updated UI state | |
| updates = { | |
| 'score_display': format_score( | |
| state.player_score, | |
| state.ai_score, | |
| state.current_round, | |
| state.total_rounds | |
| ), | |
| 'feedback_display': feedback, | |
| 'ai_button_interactive': False, | |
| 'human_button_interactive': False, | |
| 'next_button_visible': not state.game_over, | |
| 'reset_button_visible': state.game_over | |
| } | |
| return updates | |
| def handle_next_picture(state): | |
| """ | |
| Handle "Next" button click | |
| Args: | |
| state: GameState instance | |
| Returns: | |
| Updated UI component values | |
| """ | |
| # Move to next round | |
| state.next_round() | |
| # Return updated UI state | |
| updates = { | |
| 'image_display': state.current_image_path, | |
| 'feedback_display': "", | |
| 'ai_button_interactive': True, | |
| 'human_button_interactive': True, | |
| 'next_button_visible': False, | |
| 'score_display': format_score( | |
| state.player_score, | |
| state.ai_score, | |
| state.current_round, | |
| state.total_rounds | |
| ) | |
| } | |
| return updates | |
| def handle_reset_game(state, image_pool): | |
| """ | |
| Handle game reset | |
| Args: | |
| state: GameState instance | |
| image_pool: ImagePool instance | |
| Returns: | |
| Updated UI component values | |
| """ | |
| # Reset game state | |
| state.reset() | |
| # Create new game image set | |
| state.images = image_pool.create_game_set() | |
| state.current_round = 0 | |
| state.next_round() | |
| # Return initial UI state | |
| updates = { | |
| 'image_display': state.current_image_path, | |
| 'score_display': format_score(0, 0, 1, 20), | |
| 'feedback_display': "New game started! Is this AI-generated or real human?", | |
| 'ai_button_interactive': True, | |
| 'human_button_interactive': True, | |
| 'next_button_visible': False, | |
| 'reset_button_visible': True | |
| } | |
| return updates | |