Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import os | |
| from groq import Groq | |
| import json | |
| import re | |
| from typing import List, Dict, Optional | |
| import time | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| import pandas as pd | |
| from datetime import datetime, timedelta | |
| import streamlit.components.v1 as components | |
| import hashlib | |
| # Configure page | |
| st.set_page_config( | |
| page_title="🧮 MathGenius Academy", | |
| page_icon="🧮", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Custom CSS for enhanced UI | |
| def load_custom_css(): | |
| st.markdown(""" | |
| <style> | |
| /* Import Google Fonts */ | |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=Nunito:wght@300;400;500;600;700&display=swap'); | |
| /* Global Styles */ | |
| .main { | |
| font-family: 'Poppins', sans-serif; | |
| } | |
| /* Header Styling */ | |
| .hero-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 2rem; | |
| border-radius: 20px; | |
| color: white; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 20px 40px rgba(102, 126, 234, 0.3); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .hero-header::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); | |
| animation: float 6s ease-in-out infinite; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translate(0, 0) rotate(0deg); } | |
| 50% { transform: translate(20px, -20px) rotate(180deg); } | |
| } | |
| .hero-title { | |
| font-size: 3.5rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .hero-subtitle { | |
| font-size: 1.2rem; | |
| opacity: 0.9; | |
| margin-bottom: 1rem; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| /* Timer styling */ | |
| .timer-display { | |
| background: linear-gradient(135deg, #FF6B6B, #FF8E8E); | |
| color: white; | |
| padding: 1rem 2rem; | |
| border-radius: 20px; | |
| text-align: center; | |
| font-size: 2rem; | |
| font-weight: 700; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 10px 30px rgba(255, 107, 107, 0.3); | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); box-shadow: 0 10px 30px rgba(255, 107, 107, 0.3); } | |
| 50% { transform: scale(1.02); box-shadow: 0 15px 40px rgba(255, 107, 107, 0.5); } | |
| 100% { transform: scale(1); box-shadow: 0 10px 30px rgba(255, 107, 107, 0.3); } | |
| } | |
| .timer-warning { | |
| background: linear-gradient(135deg, #FF4757, #FF6B6B) !important; | |
| animation: fastPulse 1s infinite !important; | |
| } | |
| @keyframes fastPulse { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| 100% { transform: scale(1); } | |
| } | |
| /* Card Styling */ | |
| .custom-card { | |
| background: white; | |
| padding: 2rem; | |
| border-radius: 20px; | |
| box-shadow: 0 15px 35px rgba(0,0,0,0.1); | |
| border: 1px solid #e1e8ed; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| color: #333; | |
| } | |
| .custom-card:hover { | |
| transform: translateY(-10px); | |
| box-shadow: 0 25px 50px rgba(0,0,0,0.15); | |
| } | |
| .custom-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 4px; | |
| background: linear-gradient(90deg, #667eea, #764ba2); | |
| } | |
| .custom-card h4 { | |
| color: #667eea !important; | |
| margin-bottom: 1rem; | |
| font-weight: 600; | |
| } | |
| .custom-card p, .custom-card li { | |
| color: #444 !important; | |
| line-height: 1.6; | |
| } | |
| .custom-card ul { | |
| padding-left: 1.5rem; | |
| } | |
| .custom-card ol { | |
| padding-left: 1.5rem; | |
| } | |
| /* Question Card */ | |
| .question-card { | |
| background: linear-gradient(145deg, #f8fafc 0%, #e2e8f0 100%); | |
| border-radius: 20px; | |
| padding: 2rem; | |
| margin: 1rem 0; | |
| border-left: 6px solid #667eea; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .question-card:hover { | |
| transform: translateX(10px); | |
| box-shadow: 0 15px 40px rgba(0,0,0,0.15); | |
| } | |
| /* Stats Cards */ | |
| .stat-card { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 1.5rem; | |
| border-radius: 15px; | |
| text-align: center; | |
| box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3); | |
| transition: all 0.3s ease; | |
| } | |
| .stat-card:hover { | |
| transform: scale(1.05); | |
| } | |
| .stat-number { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| } | |
| .stat-label { | |
| font-size: 0.9rem; | |
| opacity: 0.9; | |
| } | |
| /* Buttons */ | |
| .custom-button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 0.75rem 2rem; | |
| border-radius: 50px; | |
| font-weight: 600; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); | |
| } | |
| .custom-button:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); | |
| } | |
| /* Progress Bar */ | |
| .progress-container { | |
| background: #e2e8f0; | |
| border-radius: 50px; | |
| padding: 0.5rem; | |
| margin: 1rem 0; | |
| } | |
| .progress-bar { | |
| background: linear-gradient(90deg, #667eea, #764ba2); | |
| height: 20px; | |
| border-radius: 50px; | |
| transition: width 1s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: 600; | |
| font-size: 0.8rem; | |
| } | |
| /* Animations */ | |
| @keyframes slideInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .slide-in-up { | |
| animation: slideInUp 0.6s ease-out; | |
| } | |
| .pulse-animation { | |
| animation: pulse 2s infinite; | |
| } | |
| /* Sidebar Styling */ | |
| .sidebar .sidebar-content { | |
| background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| /* Toast Notifications */ | |
| .toast { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 1rem 2rem; | |
| border-radius: 10px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| z-index: 1000; | |
| animation: slideInRight 0.5s ease; | |
| } | |
| @keyframes slideInRight { | |
| from { transform: translateX(100%); } | |
| to { transform: translateX(0); } | |
| } | |
| /* Mobile Responsive */ | |
| @media (max-width: 768px) { | |
| .hero-title { | |
| font-size: 2.5rem; | |
| } | |
| .custom-card { | |
| padding: 1rem; | |
| } | |
| .question-card { | |
| padding: 1rem; | |
| } | |
| .timer-display { | |
| font-size: 1.5rem; | |
| padding: 0.75rem 1.5rem; | |
| } | |
| } | |
| body, .main, .block-container { | |
| background: #f7f9fb !important; | |
| } | |
| .custom-card { | |
| margin-bottom: 2rem; | |
| padding: 2.5rem; | |
| border-radius: 24px; | |
| box-shadow: 0 8px 32px rgba(102, 126, 234, 0.08); | |
| } | |
| .question-card { | |
| background: #fff; | |
| border-radius: 20px; | |
| box-shadow: 0 4px 24px rgba(102, 126, 234, 0.10); | |
| margin-bottom: 2rem; | |
| padding: 2rem 2.5rem; | |
| } | |
| .question-card:hover { | |
| box-shadow: 0 8px 32px rgba(102, 126, 234, 0.15); | |
| } | |
| .question-card .stRadio > div { | |
| gap: 1.5rem; | |
| } | |
| .stRadio label { | |
| background: #f0f4fa; | |
| border-radius: 16px; | |
| padding: 0.75rem 1.5rem; | |
| margin-bottom: 0.5rem; | |
| font-size: 1.1rem; | |
| transition: background 0.2s, color 0.2s; | |
| cursor: pointer; | |
| } | |
| .stRadio label:hover { | |
| background: #e3e9f7; | |
| color: #667eea; | |
| } | |
| .stRadio input:checked + label { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: #fff; | |
| } | |
| .stButton > button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: #fff; | |
| border-radius: 30px; | |
| font-weight: 600; | |
| font-size: 1.1rem; | |
| padding: 0.75rem 2.5rem; | |
| margin: 0.5rem 0; | |
| box-shadow: 0 4px 16px rgba(102, 126, 234, 0.10); | |
| transition: background 0.2s, box-shadow 0.2s; | |
| } | |
| .stButton > button:hover { | |
| background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); | |
| box-shadow: 0 8px 32px rgba(102, 126, 234, 0.15); | |
| } | |
| .stMarkdown h2, .stMarkdown h3, .stMarkdown h4 { | |
| font-weight: 700; | |
| margin-top: 2rem; | |
| margin-bottom: 1rem; | |
| color: #667eea; | |
| } | |
| .stMarkdown hr { | |
| border: none; | |
| border-top: 2px solid #e1e8ed; | |
| margin: 2rem 0; | |
| } | |
| .progress-container { | |
| margin-bottom: 1.5rem; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| class MathOlympiadExam: | |
| def __init__(self): | |
| self.client = self._initialize_groq_client() | |
| self.grade_levels = { | |
| 1: {"name": "Grade 1 (Ages 6-7)", "emoji": "🌱", "color": "#FF6B6B"}, | |
| 2: {"name": "Grade 2 (Ages 7-8)", "emoji": "🌿", "color": "#4ECDC4"}, | |
| 3: {"name": "Grade 3 (Ages 8-9)", "emoji": "🍃", "color": "#45B7D1"}, | |
| 4: {"name": "Grade 4 (Ages 9-10)", "emoji": "🌳", "color": "#96CEB4"}, | |
| 5: {"name": "Grade 5 (Ages 10-11)", "emoji": "🎯", "color": "#FECA57"}, | |
| 6: {"name": "Grade 6 (Ages 11-12)", "emoji": "🚀", "color": "#FF9FF3"}, | |
| 7: {"name": "Grade 7 (Ages 12-13)", "emoji": "⭐", "color": "#54A0FF"}, | |
| 8: {"name": "Grade 8 (Ages 13-14)", "emoji": "💎", "color": "#5F27CD"}, | |
| 9: {"name": "Grade 9 (Ages 14-15)", "emoji": "🏆", "color": "#00D2D3"}, | |
| 10: {"name": "Grade 10 (Ages 15-16)", "emoji": "👑", "color": "#FF6348"} | |
| } | |
| def _initialize_groq_client(self) -> Optional[Groq]: | |
| """Initialize Groq client with API key""" | |
| try: | |
| api_key = "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" | |
| if not api_key: | |
| st.error("⚠️ GROQ_API_KEY not found!") | |
| return None | |
| return Groq(api_key=api_key) | |
| except Exception as e: | |
| st.error(f"Failed to initialize Groq client: {e}") | |
| return None | |
| def generate_questions(self, grade: int, num_questions: int, difficulty: str = "medium") -> List[Dict]: | |
| """Generate unique math olympiad questions with STRICT rules against visual questions - FIXED""" | |
| if not self.client: | |
| return [] | |
| try: | |
| difficulty_map = { | |
| 1: "Basic counting 1-20, simple addition/subtraction within 20, comparing numbers, basic money problems", | |
| 2: "Addition/subtraction within 100, word problems about time/money, simple multiplication by 2,5,10", | |
| 3: "Multiplication tables to 12, basic division, simple fractions (1/2, 1/4), word problems with multiple steps", | |
| 4: "Multi-digit multiplication/division, equivalent fractions, decimals to tenths, perimeter calculations", | |
| 5: "Advanced arithmetic, basic algebra (find the missing number), percentages, area calculations, factors/multiples", | |
| 6: "Pre-algebra equations, ratios/proportions, basic statistics (mean/median), coordinate problems", | |
| 7: "Linear equations, integers, basic probability, algebraic expressions, geometric formulas", | |
| 8: "Systems of equations, exponents/roots, advanced probability, quadratic expressions, geometric theorems", | |
| 9: "Advanced algebra, trigonometry basics, sequences/series, complex word problems, mathematical proofs", | |
| 10: "Pre-calculus topics, advanced trigonometry, logarithms, complex problem solving, advanced geometry" | |
| } | |
| difficulty_modifiers = { | |
| "easy": "Focus on fundamental concepts with straightforward calculations", | |
| "medium": "Standard olympiad difficulty with moderate complexity requiring 2-3 steps", | |
| "hard": "Advanced multi-step problems requiring deep mathematical reasoning" | |
| } | |
| topics = difficulty_map.get(grade, "general math concepts") | |
| modifier = difficulty_modifiers.get(difficulty, "") | |
| # STRICT forbidden keywords - expanded list | |
| forbidden_keywords = [ | |
| "figure", "diagram", "shape", "block", "square", "triangle", "circle", "rectangle", "polygon", | |
| "pattern below", "picture", "image", "graph", "chart", "visual", "draw", "sketch", "plot", | |
| "look at", "observe", "see the", "shown", "displayed", "illustrated", "appears", | |
| "geometric shape", "arrange", "tessellation", "symmetry", "reflection", "rotation", | |
| "hexagon", "pentagon", "octagon", "parallelogram", "rhombus", "trapezoid", | |
| "coordinate plane", "grid", "lattice", "dot", "point on", "line segment", | |
| "angle", "vertex", "side", "face", "edge", "cube", "cylinder", "sphere", | |
| "shaded", "colored", "pattern", "sequence of shapes", "visual pattern" | |
| ] | |
| seen_hashes = getattr(st.session_state, 'question_hashes', set()) | |
| # ENHANCED prompt with VERY strict formatting requirements | |
| base_prompt = f""" | |
| Generate exactly {num_questions} UNIQUE Math Olympiad questions for Grade {grade}. | |
| 🚨 ABSOLUTE RESTRICTIONS - NEVER INCLUDE: | |
| ❌ Any visual elements (shapes, figures, diagrams, patterns, images) | |
| ❌ Questions requiring visual interpretation | |
| ❌ Geometric shape identification or counting | |
| ❌ Pattern completion with visual elements | |
| ❌ Any reference to "figure", "diagram", "shape", "pattern below" | |
| ❌ Coordinate plotting or graph reading | |
| ❌ Any visual arrangement problems | |
| ✅ ONLY ALLOWED - TEXT-BASED QUESTIONS: | |
| ✅ Pure arithmetic calculations and word problems | |
| ✅ Number sequences (like: 2, 4, 6, 8, ?) | |
| ✅ Logic puzzles with numbers/text only | |
| ✅ Real-world scenarios (money, time, travel, shopping) | |
| ✅ Algebraic word problems | |
| ✅ Statistical problems with given data | |
| ✅ Mathematical reasoning without visuals | |
| TOPICS: {topics} | |
| DIFFICULTY: {modifier} | |
| 🎯 CRITICAL: Answer must be EXACTLY one letter: A, B, C, or D (no explanations in answer line) | |
| FORMAT (EXACT - NO DEVIATIONS): | |
| **Question 1:** [Engaging word problem - NO VISUALS] | |
| A) [Option A with number/calculation] | |
| B) [Option B with number/calculation] | |
| C) [Option C with number/calculation] | |
| D) [Option D with number/calculation] | |
| **Answer:** A | |
| **Explanation:** [Step-by-step solution] | |
| **Hint:** [Helpful tip for solving] | |
| **Question 2:** [Next question...] | |
| A) [Option A] | |
| B) [Option B] | |
| C) [Option C] | |
| D) [Option D] | |
| **Answer:** C | |
| **Explanation:** [Step-by-step solution] | |
| **Hint:** [Helpful tip for solving] | |
| [Continue for all {num_questions} questions] | |
| 🔥 IMPORTANT FORMATTING RULES: | |
| - Answer line must contain ONLY the letter (A, B, C, or D) | |
| - No parentheses, explanations, or additional text in the answer | |
| - Each option must start with the letter followed by closing parenthesis | |
| - Keep questions practical and engaging with real scenarios | |
| MAKE QUESTIONS PRACTICAL AND ENGAGING - Use real scenarios students can relate to! | |
| """ | |
| max_attempts = 3 | |
| all_questions = [] | |
| for attempt in range(max_attempts): | |
| try: | |
| response = self.client.chat.completions.create( | |
| model="openai/gpt-oss-120b", # More reliable model | |
| messages=[ | |
| {"role": "system", "content": "You are an expert Math Olympiad question creator. You MUST create only text-based word problems without any visual elements. The answer must be EXACTLY one letter: A, B, C, or D with no additional text or explanations."}, | |
| {"role": "user", "content": base_prompt} | |
| ], | |
| temperature=0.7, # Balanced creativity | |
| max_tokens=4000 | |
| ) | |
| new_questions = self._parse_questions_enhanced(response.choices[0].message.content) | |
| # STRICT filtering | |
| valid_questions = [] | |
| for q in new_questions: | |
| q_text = q['question_text'].lower() | |
| # Check for forbidden keywords | |
| if any(forbidden in q_text for forbidden in forbidden_keywords): | |
| continue | |
| # Additional checks for visual elements | |
| if any(word in q_text for word in ['draw', 'sketch', 'plot', 'shown', 'displayed', 'appears']): | |
| continue | |
| # Check uniqueness | |
| q_hash = self._get_question_hash(q['question_text'] + str(q['options'])) | |
| if q_hash not in seen_hashes: | |
| q['hash'] = q_hash | |
| valid_questions.append(q) | |
| seen_hashes.add(q_hash) | |
| all_questions.extend(valid_questions) | |
| if len(all_questions) >= num_questions: | |
| break | |
| # Add retry hint with more specific guidance | |
| base_prompt += f"\n\nNEED MORE QUESTIONS: Generated {len(all_questions)} so far. Create MORE PRACTICAL WORD PROBLEMS about real-life scenarios (shopping, travel, sports, cooking, etc.). REMEMBER: Answer must be EXACTLY one letter (A, B, C, or D)." | |
| except Exception as e: | |
| st.error(f"Error in attempt {attempt + 1}: {e}") | |
| continue | |
| st.session_state.question_hashes = seen_hashes | |
| final_questions = all_questions[:num_questions] | |
| if len(final_questions) < num_questions: | |
| st.warning(f"⚠️ Generated {len(final_questions)} unique valid questions (requested {num_questions}). Some were filtered out for containing visual elements.") | |
| return final_questions | |
| except Exception as e: | |
| st.error(f"Failed to generate questions: {e}") | |
| return [] | |
| def _get_question_hash(self, question_text: str) -> str: | |
| """Generate hash for question to track uniqueness""" | |
| # Remove numbers and normalize text for better uniqueness detection | |
| normalized = re.sub(r'\d+', 'X', question_text.lower().strip()) | |
| normalized = re.sub(r'[^\w\s]', '', normalized) # Remove punctuation | |
| return hashlib.md5(normalized.encode()).hexdigest() | |
| def _parse_questions_enhanced(self, content: str) -> List[Dict]: | |
| """Enhanced question parsing with better error handling and validation""" | |
| questions = [] | |
| # Split by question markers | |
| question_blocks = re.split(r'\*\*Question \d+:\*\*', content) | |
| if len(question_blocks) < 2: | |
| # Try alternative splitting | |
| question_blocks = re.split(r'Question \d+:', content) | |
| for i, block in enumerate(question_blocks[1:], 1): | |
| try: | |
| lines = [line.strip() for line in block.strip().split('\n') if line.strip()] | |
| if len(lines) < 6: # Need at least question + 4 options + answer | |
| continue | |
| question_text = lines[0].strip() | |
| if not question_text: | |
| continue | |
| # Parse options - FIXED: Create both dictionary and list properly | |
| options_dict = {} | |
| options_list = [] | |
| correct_answer = None | |
| explanation = "" | |
| hint = "" | |
| current_section = "options" | |
| for line in lines[1:]: | |
| line = line.strip() | |
| # Detect sections | |
| if line.startswith('**Answer:**') or line.startswith('**Correct Answer:**'): | |
| current_section = "answer" | |
| answer_part = line.replace('**Answer:**', '').replace('**Correct Answer:**', '').strip() | |
| if answer_part: | |
| # Extract only the letter from the answer (A, B, C, or D) | |
| answer_match = re.search(r'[ABCD]', answer_part.upper()) | |
| if answer_match: | |
| correct_answer = answer_match.group() | |
| continue | |
| elif line.startswith('**Explanation:**'): | |
| current_section = "explanation" | |
| exp_part = line.replace('**Explanation:**', '').strip() | |
| if exp_part: | |
| explanation = exp_part | |
| continue | |
| elif line.startswith('**Hint:**'): | |
| current_section = "hint" | |
| hint_part = line.replace('**Hint:**', '').strip() | |
| if hint_part: | |
| hint = hint_part | |
| continue | |
| # Process based on current section | |
| if current_section == "options" and line.startswith(('A)', 'B)', 'C)', 'D)')): | |
| option_key = line[0] # A, B, C, or D | |
| option_text = line[2:].strip() # Remove "A) " | |
| options_dict[option_key] = option_text | |
| options_list.append(line) # Keep full format "A) text" | |
| elif current_section == "answer": | |
| # Try to extract single letter answer | |
| if line.strip() in ['A', 'B', 'C', 'D']: | |
| correct_answer = line.strip() | |
| elif not correct_answer: | |
| # Fallback: extract first letter found | |
| answer_match = re.search(r'[ABCD]', line.upper()) | |
| if answer_match: | |
| correct_answer = answer_match.group() | |
| elif current_section == "explanation" and line: | |
| if explanation: | |
| explanation += " " + line | |
| else: | |
| explanation = line | |
| elif current_section == "hint" and line: | |
| if hint: | |
| hint += " " + line | |
| else: | |
| hint = line | |
| # Validate question - ensure we have exactly 4 options and a valid answer | |
| if len(options_dict) == 4 and correct_answer and correct_answer in options_dict: | |
| questions.append({ | |
| 'question_number': i, | |
| 'question_text': question_text, | |
| 'options': options_list, # List format for compatibility: ['A) text', 'B) text', ...] | |
| 'options_dict': options_dict, # Dictionary format: {'A': 'text', 'B': 'text', ...} | |
| 'correct_answer': correct_answer, | |
| 'explanation': explanation or "Work through the problem step by step.", | |
| 'hint': hint or "Read the question carefully and identify what you need to find.", | |
| 'user_answer': None, | |
| 'time_taken': 0, | |
| 'difficulty': self._estimate_difficulty(question_text) | |
| }) | |
| else: | |
| st.warning(f"Question {i} validation failed: options={len(options_dict)}, answer='{correct_answer}', keys={list(options_dict.keys())}") | |
| except Exception as e: | |
| st.warning(f"Error parsing question {i}: {e}") | |
| continue | |
| return questions | |
| def _estimate_difficulty(self, question_text: str) -> str: | |
| """Estimate question difficulty based on content""" | |
| hard_keywords = ['system', 'equation', 'quadratic', 'prove', 'theorem', 'complex', 'advanced'] | |
| easy_keywords = ['add', 'subtract', 'simple', 'basic', 'find the', 'what is', 'how many'] | |
| text_lower = question_text.lower() | |
| if any(keyword in text_lower for keyword in hard_keywords): | |
| return "Hard" | |
| elif any(keyword in text_lower for keyword in easy_keywords): | |
| return "Easy" | |
| else: | |
| return "Medium" | |
| # Initialize session state | |
| def initialize_session_state(): | |
| if 'questions' not in st.session_state: | |
| st.session_state.questions = [] | |
| if 'exam_completed' not in st.session_state: | |
| st.session_state.exam_completed = False | |
| if 'grade_selected' not in st.session_state: | |
| st.session_state.grade_selected = None | |
| if 'exam_history' not in st.session_state: | |
| st.session_state.exam_history = [] | |
| if 'current_question' not in st.session_state: | |
| st.session_state.current_question = 0 | |
| if 'exam_mode' not in st.session_state: | |
| st.session_state.exam_mode = 'practice' | |
| if 'start_time' not in st.session_state: | |
| st.session_state.start_time = None | |
| if 'user_profile' not in st.session_state: | |
| st.session_state.user_profile = { | |
| 'name': 'Math Explorer', | |
| 'total_exams': 0, | |
| 'total_questions': 0, | |
| 'correct_answers': 0, | |
| 'favorite_grade': 5, | |
| 'badges': [], | |
| 'streak': 0 | |
| } | |
| if 'question_hashes' not in st.session_state: | |
| st.session_state.question_hashes = set() | |
| def get_exam_system(): | |
| return MathOlympiadExam() | |
| def render_hero_section(): | |
| """Render the hero section with animations""" | |
| st.markdown(""" | |
| <div class="hero-header slide-in-up"> | |
| <div class="hero-title">🧮 MathGenius Academy</div> | |
| <div class="hero-subtitle">Master Mathematics Through Interactive Olympiad Training</div> | |
| <div style="margin-top: 1rem;"> | |
| <span style="background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 25px; margin: 0 0.5rem;">🎯 Practical Problems</span> | |
| <span style="background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 25px; margin: 0 0.5rem;">📊 Progress Tracking</span> | |
| <span style="background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 25px; margin: 0 0.5rem;">🏆 Achievement System</span> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html = True) | |
| def render_timer_display(): | |
| """Render enhanced timer display with better positioning""" | |
| if st.session_state.exam_mode == 'timed' and st.session_state.start_time: | |
| total_time = len(st.session_state.questions) * 60 # 1 minute per question | |
| elapsed = time.time() - st.session_state.start_time | |
| remaining = max(0, total_time - elapsed) | |
| mins, secs = divmod(int(remaining), 60) | |
| # Add warning class if time is running low | |
| warning_class = "timer-warning" if remaining <= 60 else "" | |
| timer_html = f""" | |
| <div class="timer-display {warning_class}"> | |
| <div style="display: flex; align-items: center; justify-content: center; gap: 1rem;"> | |
| <div style="font-size: 3rem;">⏰</div> | |
| <div> | |
| <div style="font-size: 2.5rem; font-weight: 800;">{mins:02d}:{secs:02d}</div> | |
| <div style="font-size: 1rem; opacity: 0.9;">Time Remaining</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| st.markdown(timer_html, unsafe_allow_html=True) | |
| # Auto-submit when time is up | |
| if remaining <= 0: | |
| st.error("⏰ Time's up! Auto-submitting your exam...") | |
| st.session_state.exam_completed = True | |
| time.sleep(1) | |
| st.rerun() | |
| # Show warning when time is low | |
| elif remaining <= 60: | |
| st.warning(f"⚠️ Only {int(remaining)} seconds left!") | |
| def render_dashboard(): | |
| """Render user dashboard with statistics""" | |
| profile = st.session_state.user_profile | |
| st.markdown("### 📊 Your Learning Dashboard") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.markdown(f""" | |
| <div class="stat-card"> | |
| <div class="stat-number">{profile['total_exams']}</div> | |
| <div class="stat-label">Exams Taken</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col2: | |
| percentage = (profile['correct_answers'] / max(profile['total_questions'], 1)) * 100 | |
| st.markdown(f""" | |
| <div class="stat-card"> | |
| <div class="stat-number">{percentage:.1f}%</div> | |
| <div class="stat-label">percentage</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col3: | |
| st.markdown(f""" | |
| <div class="stat-card"> | |
| <div class="stat-number">{profile['streak']}</div> | |
| <div class="stat-label">Day Streak</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col4: | |
| st.markdown(f""" | |
| <div class="stat-card"> | |
| <div class="stat-number">{len(profile['badges'])}</div> | |
| <div class="stat-label">Badges Earned</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Progress Chart | |
| if st.session_state.exam_history: | |
| df = pd.DataFrame(st.session_state.exam_history) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("#### 📈 Performance Trend") | |
| fig = px.line(df, x='date', y='percentage', | |
| title='Your Progress Over Time', | |
| color_discrete_sequence=['#667eea']) | |
| fig.update_layout( | |
| plot_bgcolor='rgba(0,0,0,0)', | |
| paper_bgcolor='rgba(0,0,0,0)', | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| def render_question_card(question, question_index, total_questions): | |
| """Render an individual question with enhanced UI - FIXED""" | |
| progress = ((question_index + 1) / total_questions) * 100 | |
| st.markdown(f""" | |
| <div class="progress-container"> | |
| <div class="progress-bar" style="width: {progress}%;"> | |
| Question {question_index + 1} of {total_questions} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div class="question-card slide-in-up"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> | |
| <h3 style="color: #667eea; margin: 0;">Question {question['question_number']}</h3> | |
| <span style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 0.25rem 0.75rem; border-radius: 15px; font-size: 0.8rem;"> | |
| {question.get('difficulty', 'Medium')} Level | |
| </span> | |
| </div> | |
| <div style="font-size: 1.1rem; font-weight: 500; margin-bottom: 1.5rem; line-height: 1.6; color: #222;"> | |
| {question['question_text']} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # FIXED: Handle both list and dictionary options formats | |
| if isinstance(question['options'], list): | |
| # List format: ['A) text', 'B) text', ...] | |
| option_keys = [] | |
| option_labels = [] | |
| for opt in question['options']: | |
| if ')' in opt: | |
| key, label = opt.split(')', 1) | |
| option_keys.append(key.strip()) | |
| option_labels.append(label.strip()) | |
| # Create format function for radio buttons | |
| def format_option(key): | |
| try: | |
| idx = option_keys.index(key) | |
| return f"{key}) {option_labels[idx]}" | |
| except (ValueError, IndexError): | |
| return f"{key}) Option not found" | |
| selected = st.radio( | |
| "Choose your answer:", | |
| options=option_keys, | |
| format_func=format_option, | |
| key=f"q_{question_index}", | |
| index=None | |
| ) | |
| elif isinstance(question['options'], dict): | |
| # Dictionary format: {'A': 'text', 'B': 'text', ...} | |
| option_keys = list(question['options'].keys()) | |
| def format_option_dict(key): | |
| return f"{key}) {question['options'].get(key, 'Option not found')}" | |
| selected = st.radio( | |
| "Choose your answer:", | |
| options=option_keys, | |
| format_func=format_option_dict, | |
| key=f"q_{question_index}", | |
| index=None | |
| ) | |
| else: | |
| st.error("Invalid options format in question data") | |
| return None | |
| # Hint system | |
| if st.button(f"💡 Need a hint?", key=f"hint_{question_index}"): | |
| if question.get('hint'): | |
| st.info(f"💡 **Hint:** {question['hint']}") | |
| else: | |
| st.info("💡 **Hint:** Try breaking down the problem step by step!") | |
| return selected | |
| def render_exam_interface(): | |
| """Render the main exam interface with improved timer - FIXED""" | |
| questions = st.session_state.questions | |
| total_questions = len(questions) | |
| # Display timer at the top for timed mode | |
| render_timer_display() | |
| # Question navigation | |
| if total_questions > 1: | |
| st.markdown("#### 📍 Question Navigation") | |
| cols = st.columns(min(10, total_questions)) | |
| for i in range(total_questions): | |
| with cols[i % 10]: | |
| status = "✅" if questions[i]['user_answer'] else "⭕" | |
| if st.button(f"{status} Q{i+1}", key=f"nav_{i}"): | |
| st.session_state.current_question = i | |
| # Current question | |
| current_q = st.session_state.current_question | |
| if current_q < total_questions: | |
| # FIXED: Call render_question_card as a standalone function | |
| selected = render_question_card(questions[current_q], current_q, total_questions) | |
| questions[current_q]['user_answer'] = selected | |
| # Navigation buttons | |
| col1, col2, col3 = st.columns([1, 2, 1]) | |
| with col1: | |
| if current_q > 0: | |
| if st.button("⬅️ Previous", key="prev_btn"): | |
| st.session_state.current_question -= 1 | |
| st.rerun() | |
| with col3: | |
| if current_q < total_questions - 1: | |
| if st.button("Next ➡️", key="next_btn"): | |
| st.session_state.current_question += 1 | |
| st.rerun() | |
| else: | |
| if st.button("🎯 Submit Exam", key="submit_btn", type="primary"): | |
| unanswered = [i+1 for i, q in enumerate(questions) if not q['user_answer']] | |
| if unanswered: | |
| st.error(f"⚠️ Please answer all questions. Missing: {', '.join(map(str, unanswered))}") | |
| else: | |
| st.session_state.exam_completed = True | |
| st.rerun() | |
| def calculate_badges(score, total, grade): | |
| """Calculate badges earned based on performance""" | |
| percentage = (score / total) * 100 | |
| badges = [] | |
| if percentage == 100: | |
| badges.append("🏆 Perfect Score") | |
| elif percentage >= 90: | |
| badges.append("⭐ Excellence") | |
| elif percentage >= 80: | |
| badges.append("🎯 High Achiever") | |
| elif percentage >= 70: | |
| badges.append("📚 Good Progress") | |
| if grade >= 8: | |
| badges.append("🚀 Advanced Level") | |
| return badges | |
| def render_results_section(): | |
| """Render enhanced results with animations and insights""" | |
| st.markdown("---") | |
| st.markdown("### 🎉 Exam Results") | |
| questions = st.session_state.questions | |
| correct_count = sum(1 for q in questions if q['user_answer'] == q['correct_answer']) | |
| total_questions = len(questions) | |
| percentage = (correct_count / total_questions) * 100 | |
| # Update user profile | |
| profile = st.session_state.user_profile | |
| profile['total_exams'] += 1 | |
| profile['total_questions'] += total_questions | |
| profile['correct_answers'] += correct_count | |
| # Add to history | |
| st.session_state.exam_history.append({ | |
| 'date': datetime.now(), | |
| 'grade': st.session_state.grade_selected, | |
| 'score': correct_count, | |
| 'total': total_questions, | |
| 'percentage': percentage | |
| }) | |
| # Calculate badges | |
| new_badges = calculate_badges(correct_count, total_questions, st.session_state.grade_selected) | |
| profile['badges'].extend(new_badges) | |
| # Animated score reveal | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.markdown(f""" | |
| <div class="stat-card pulse-animation"> | |
| <div class="stat-number">{correct_count}/{total_questions}</div> | |
| <div class="stat-label">Final Score</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col2: | |
| st.markdown(f""" | |
| <div class="stat-card pulse-animation"> | |
| <div class="stat-number">{percentage:.1f}%</div> | |
| <div class="stat-label">percentage</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col3: | |
| if percentage >= 90: | |
| grade_emoji = "🏆" | |
| grade_text = "Excellent!" | |
| elif percentage >= 80: | |
| grade_emoji = "⭐" | |
| grade_text = "Great Job!" | |
| elif percentage >= 70: | |
| grade_emoji = "👍" | |
| grade_text = "Good Work!" | |
| else: | |
| grade_emoji = "📚" | |
| grade_text = "Keep Practicing!" | |
| st.markdown(f""" | |
| <div class="stat-card pulse-animation"> | |
| <div class="stat-number">{grade_emoji}</div> | |
| <div class="stat-label">{grade_text}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # New badges notification | |
| if new_badges: | |
| st.success(f"🎉 New badges earned: {', '.join(new_badges)}") | |
| # Detailed question review | |
| st.markdown("#### 📝 Question Review") | |
| for i, question in enumerate(questions): | |
| is_correct = question['user_answer'] == question['correct_answer'] | |
| with st.expander(f"Question {i+1} - {'✅ Correct' if is_correct else '❌ Incorrect'} ({question.get('difficulty', 'Medium')} Level)"): | |
| st.markdown(f"**Question:** {question['question_text']}") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**Your Answer:** {question['user_answer']}") | |
| with col2: | |
| st.markdown(f"**Correct Answer:** {question['correct_answer']}") | |
| if question.get('explanation'): | |
| st.markdown(f"**Explanation:** {question['explanation']}") | |
| if not is_correct and question.get('hint'): | |
| st.info(f"💡 **Hint for next time:** {question['hint']}") | |
| # Performance insights with bullet points | |
| exam_system = get_exam_system() | |
| if exam_system.client: | |
| st.markdown("#### 🧠 AI Performance Analysis") | |
| with st.spinner("Generating personalized insights..."): | |
| summary = generate_enhanced_summary( | |
| st.session_state.grade_selected, | |
| questions, | |
| correct_count, | |
| total_questions, | |
| exam_system | |
| ) | |
| st.markdown(f""" | |
| <div class="custom-card"> | |
| {summary} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Show completion message | |
| st.markdown("#### 🎓 Exam Complete!") | |
| st.info("Great job completing the exam! Use the sidebar to generate a new exam or check out the Dashboard tab to view your progress.") | |
| def generate_enhanced_summary(grade, questions, score, total, exam_system): | |
| """Generate enhanced AI-powered performance summary in bullet points""" | |
| try: | |
| # Analyze performance patterns | |
| correct_questions = [q for q in questions if q['user_answer'] == q['correct_answer']] | |
| incorrect_questions = [q for q in questions if q['user_answer'] != q['correct_answer']] | |
| difficulty_analysis = {} | |
| for q in questions: | |
| diff = q.get('difficulty', 'Medium') | |
| if diff not in difficulty_analysis: | |
| difficulty_analysis[diff] = {'correct': 0, 'total': 0} | |
| difficulty_analysis[diff]['total'] += 1 | |
| if q['user_answer'] == q['correct_answer']: | |
| difficulty_analysis[diff]['correct'] += 1 | |
| percentage = (score / total * 100) if total > 0 else 0 | |
| prompt = f"""Analyze this Math Olympiad performance for a Grade {grade} student: | |
| PERFORMANCE SUMMARY: | |
| - Score: {score}/{total} ({percentage:.1f}%) | |
| - Difficulty Breakdown: {difficulty_analysis} | |
| SAMPLE INCORRECT QUESTIONS: | |
| {chr(10).join([f"- {q['question_text'][:100]}..." for q in incorrect_questions[:2]])} | |
| Please provide a summary in BULLET POINTS with emojis using this exact format: | |
| 🎯 **Strengths:** | |
| • [strength 1 with emoji] | |
| • [strength 2 with emoji] | |
| 📈 **Areas to Improve:** | |
| • [area to improve 1 with emoji] | |
| • [area to improve 2 with emoji] | |
| 💡 **Quick Tips:** | |
| • [tip 1 with emoji] | |
| • [tip 2 with emoji] | |
| 🌟 **Motivation:** | |
| • [encouraging message with emoji] | |
| Use Grade {grade} appropriate language and keep each bullet point short and encouraging. Include relevant emojis for each point.""" | |
| response = exam_system.client.chat.completions.create( | |
| model="openai/gpt-oss-120b", | |
| messages=[ | |
| {"role": "system", "content": "You are an expert Math Olympiad mentor providing detailed, encouraging feedback in bullet points with emojis."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| temperature=0, | |
| max_tokens=1000 | |
| ) | |
| return response.choices[0].message.content | |
| except Exception as e: | |
| return f""" | |
| 🎯 **Strengths:** | |
| • ✨ You completed {score} out of {total} questions correctly ({percentage:.1f}%) | |
| • 💪 You showed good problem-solving skills | |
| 📈 **Areas to Improve:** | |
| • 📚 Review the questions you missed for better understanding | |
| • ⏰ Work on time management if needed | |
| 💡 **Quick Tips:** | |
| • 🔍 Read each question carefully before answering | |
| • 💭 Use hints when you're unsure | |
| 🌟 **Motivation:** | |
| • 🎉 Great effort! Keep practicing and you'll improve even more! | |
| """ | |
| def reset_exam(): | |
| """Reset exam state for new attempt""" | |
| st.session_state.questions = [] | |
| st.session_state.exam_completed = False | |
| st.session_state.current_question = 0 | |
| st.session_state.start_time = None | |
| st.rerun() | |
| def render_sidebar(): | |
| """Enhanced sidebar with user profile and settings""" | |
| exam_system = get_exam_system() | |
| with st.sidebar: | |
| # User profile section | |
| st.markdown(f""" | |
| <div style="text-align: center; padding: 1rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 15px; color: white; margin-bottom: 1rem;"> | |
| <div style="font-size: 3rem; margin-bottom: 0.5rem;">👋</div> | |
| <div style="font-size: 1.2rem; font-weight: 600;">{st.session_state.user_profile['name']}</div> | |
| <div style="font-size: 0.9rem; opacity: 0.9;">Level {st.session_state.user_profile['favorite_grade']} Explorer</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("### ⚙️ Exam Configuration") | |
| # Grade selection with enhanced UI | |
| grade_options = list(exam_system.grade_levels.keys()) | |
| grade_labels = [f"{exam_system.grade_levels[g]['emoji']} {exam_system.grade_levels[g]['name']}" for g in grade_options] | |
| grade = st.selectbox( | |
| "🎯 Select Grade Level:", | |
| options=grade_options, | |
| format_func=lambda x: f"{exam_system.grade_levels[x]['emoji']} {exam_system.grade_levels[x]['name']}", | |
| key="grade_selector", | |
| help="Choose your current grade level for appropriate difficulty" | |
| ) | |
| # Exam mode | |
| exam_mode = st.radio( | |
| "📝 Exam Mode:", | |
| ["practice", "timed", "challenge"], | |
| format_func=lambda x: { | |
| "practice": "🎯 Practice Mode", | |
| "timed": "⏰ Timed Mode (1 min/question)", | |
| "challenge": "🔥 Challenge Mode" | |
| }[x], | |
| help="Practice: No time limit | Timed: 1 minute per question | Challenge: Extra hard questions" | |
| ) | |
| st.session_state.exam_mode = exam_mode | |
| # Number of questions | |
| if exam_mode == "challenge": | |
| num_questions = st.slider("📊 Questions:", 5, 20, 10, help="Challenge mode: 5-20 questions") | |
| else: | |
| num_questions = st.slider("📊 Questions:", 3, 15, 5, help="Choose number of questions") | |
| # Show timer info for timed mode | |
| if exam_mode == "timed": | |
| st.info(f"⏰ Total time: {num_questions} minutes ({num_questions} questions × 1 min each)") | |
| # Difficulty level | |
| difficulty = st.select_slider( | |
| "⚡ Difficulty:", | |
| options=["easy", "medium", "hard"], | |
| value="medium", | |
| format_func=lambda x: { | |
| "easy": "🟢 Easy", | |
| "medium": "🟡 Medium", | |
| "hard": "🔴 Hard" | |
| }[x] | |
| ) | |
| # Generate exam button | |
| if st.button("🚀 Generate New Exam", type="primary", use_container_width=True): | |
| if not exam_system.client: | |
| st.error("🚫 API not available!") | |
| else: | |
| with st.spinner(f"🎭 Creating your {exam_mode} exam with unique questions..."): | |
| # Add loading animation | |
| progress_bar = st.progress(0) | |
| for i in range(100): | |
| time.sleep(0.01) | |
| progress_bar.progress(i + 1) | |
| challenge_difficulty = "hard" if exam_mode == "challenge" else difficulty | |
| questions = exam_system.generate_questions(grade, num_questions, challenge_difficulty) | |
| if questions: | |
| st.session_state.questions = questions | |
| st.session_state.exam_completed = False | |
| st.session_state.grade_selected = grade | |
| st.session_state.current_question = 0 | |
| st.session_state.start_time = time.time() if exam_mode == "timed" else None | |
| st.success(f"✨ Generated {len(st.session_state.questions)} unique questions!") | |
| st.balloons() | |
| else: | |
| st.error("❌ Failed to generate exam. Try again!") | |
| st.markdown("---") | |
| # Quick stats | |
| profile = st.session_state.user_profile | |
| st.markdown("### 📈 Quick Stats") | |
| st.metric("🎯 Total Exams", profile['total_exams']) | |
| if profile['total_questions'] > 0: | |
| percentage = (profile['correct_answers'] / profile['total_questions']) * 100 | |
| st.metric("🎪 percentage", f"{percentage:.1f}%") | |
| st.metric("🔥 Streak", f"{profile['streak']} days") | |
| # Recent badges | |
| if profile['badges']: | |
| st.markdown("### 🏆 Recent Badges") | |
| for badge in profile['badges'][-3:]: | |
| st.markdown(f"- {badge}") | |
| st.markdown("---") | |
| # Settings | |
| with st.expander("⚙️ Settings"): | |
| new_name = st.text_input("Your Name:", value=profile['name']) | |
| if new_name != profile['name']: | |
| profile['name'] = new_name | |
| st.success("Name updated!") | |
| if st.button("🗑️ Reset Progress"): | |
| if st.checkbox("I'm sure"): | |
| initialize_session_state() | |
| st.success("Progress reset!") | |
| st.rerun() | |
| def render_welcome_screen(): | |
| """Render welcome screen for new users""" | |
| # Main welcome container | |
| st.markdown(""" | |
| <div class="custom-card slide-in-up" style="text-align: center; padding: 3rem;"> | |
| <div style="font-size: 4rem; margin-bottom: 1rem;">🌟</div> | |
| <h2 style="color: #667eea; margin-bottom: 1rem;">Welcome to MathGenius Academy!</h2> | |
| <p style="font-size: 1.2rem; color: #666; margin-bottom: 2rem;"> | |
| Ready to embark on an exciting mathematical journey? Let's discover your potential! | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Feature cards using Streamlit columns for better compatibility | |
| st.markdown("### ✨ What Makes Us Special") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.markdown(""" | |
| <div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 1.5rem; border-radius: 15px; text-align: center; height: 200px; display: flex; flex-direction: column; justify-content: center;"> | |
| <div style="font-size: 3rem; margin-bottom: 1rem;">🎯</div> | |
| <h4 style="margin: 0.5rem 0; color: white;">Unique Questions</h4> | |
| <p style="margin: 0; color: rgba(255,255,255,0.9);">Always new, never repeated</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col2: | |
| st.markdown(""" | |
| <div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 1.5rem; border-radius: 15px; text-align: center; height: 200px; display: flex; flex-direction: column; justify-content: center;"> | |
| <div style="font-size: 3rem; margin-bottom: 1rem;">📝</div> | |
| <h4 style="margin: 0.5rem 0; color: white;">Text-Based Only</h4> | |
| <p style="margin: 0; color: rgba(255,255,255,0.9);">Pure math, no visual puzzles</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col3: | |
| st.markdown(""" | |
| <div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 1.5rem; border-radius: 15px; text-align: center; height: 200px; display: flex; flex-direction: column; justify-content: center;"> | |
| <div style="font-size: 3rem; margin-bottom: 1rem;">⏰</div> | |
| <h4 style="margin: 0.5rem 0; color: white;">Smart Timer</h4> | |
| <p style="margin: 0; color: rgba(255,255,255,0.9);">1 minute per question</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Getting started message | |
| st.markdown("---") | |
| st.info("👈 Choose your grade level from the sidebar and click 'Generate New Exam' to begin your mathematical adventure!") | |
| # Quick start tips | |
| with st.expander("🚀 Quick Start Guide"): | |
| st.markdown(""" | |
| **Ready to begin? Follow these simple steps:** | |
| 1. **🎯 Choose Your Grade**: Select your current grade level from the sidebar | |
| 2. **📝 Pick Your Mode**: | |
| - 🟢 **Practice Mode**: No time pressure, perfect for learning | |
| - ⏰ **Timed Mode**: 1 minute per question challenge | |
| - 🔥 **Challenge Mode**: Extra difficult questions for advanced learners | |
| 3. **⚡ Set Difficulty**: Easy, Medium, or Hard - pick what feels right | |
| 4. **📊 Choose Questions**: 3-20 questions depending on your time | |
| 5. **🚀 Generate & Start**: Hit the generate button and begin! | |
| **💡 Pro Tips:** | |
| - Start with Practice mode to get comfortable | |
| - Use hints when you're stuck - learning is the goal! | |
| - Review all explanations to understand concepts better | |
| - Track your progress in the Dashboard tab | |
| """) | |
| # Motivational quote | |
| st.markdown(""" | |
| <div style="text-align: center; margin: 2rem 0; padding: 2rem; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 20px; color: white;"> | |
| <h3 style="margin: 0; color: white;">"Mathematics is not about numbers, equations, or algorithms. It is about understanding." - William Paul Thurston</h3> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| def main(): | |
| # Load custom CSS | |
| load_custom_css() | |
| # Initialize session state | |
| initialize_session_state() | |
| # Get exam system | |
| exam_system = get_exam_system() | |
| # Render hero section | |
| render_hero_section() | |
| # Main navigation | |
| tab1, tab2, tab3 = st.tabs(["🎯 Exam", "📊 Dashboard", "ℹ️ About"]) | |
| with tab1: | |
| # Render sidebar | |
| render_sidebar() | |
| # Main exam interface | |
| if not st.session_state.questions: | |
| render_welcome_screen() | |
| elif st.session_state.exam_completed: | |
| render_results_section() | |
| else: | |
| st.markdown("### 🧮 Math Olympiad Challenge") | |
| # Show exam info | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.info(f"📚 Grade {st.session_state.grade_selected}") | |
| with col2: | |
| if st.session_state.exam_mode == "timed": | |
| total_time = len(st.session_state.questions) | |
| st.info(f"⏱️ {st.session_state.exam_mode.title()} Mode ({total_time} min)") | |
| else: | |
| st.info(f"⏱️ {st.session_state.exam_mode.title()} Mode") | |
| with col3: | |
| st.info(f"📝 {len(st.session_state.questions)} Questions") | |
| render_exam_interface() | |
| with tab2: | |
| render_dashboard() | |
| # Additional dashboard features | |
| if st.session_state.exam_history: | |
| st.markdown("#### 🎯 Detailed Analytics") | |
| df = pd.DataFrame(st.session_state.exam_history) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("##### 📊 Score Distribution") | |
| fig = px.histogram(df, x='percentage', nbins=10, | |
| title='Score Distribution', | |
| color_discrete_sequence=['#667eea']) | |
| fig.update_layout(plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)') | |
| st.plotly_chart(fig, use_container_width=True) | |
| with col2: | |
| st.markdown("##### 📈 Improvement Trend") | |
| df_trend = df.copy() | |
| df_trend['moving_avg'] = df_trend['percentage'].rolling(window=3, min_periods=1).mean() | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter(x=df_trend['date'], y=df_trend['percentage'], | |
| mode='markers+lines', name='Actual Score', | |
| line=dict(color='#667eea'))) | |
| fig.add_trace(go.Scatter(x=df_trend['date'], y=df_trend['moving_avg'], | |
| mode='lines', name='Trend', | |
| line=dict(color='#764ba2', dash='dash'))) | |
| fig.update_layout(title='Performance Trend', | |
| plot_bgcolor='rgba(0,0,0,0)', | |
| paper_bgcolor='rgba(0,0,0,0)') | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Achievement showcase | |
| profile = st.session_state.user_profile | |
| if profile['badges']: | |
| st.markdown("#### 🏆 Your Achievements") | |
| badge_cols = st.columns(min(4, len(profile['badges']))) | |
| for i, badge in enumerate(profile['badges'][-8:]): # Show last 8 badges | |
| with badge_cols[i % 4]: | |
| st.markdown(f""" | |
| <div style="text-align: center; background: linear-gradient(135deg, #667eea, #764ba2); | |
| color: white; padding: 1rem; border-radius: 15px; margin-bottom: 1rem;"> | |
| <div style="font-size: 1.5rem; margin-bottom: 0.5rem;">{badge.split()[0]}</div> | |
| <div style="font-size: 0.8rem;">{' '.join(badge.split()[1:])}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with tab3: | |
| st.markdown("# 📖 About MathGenius Academy") | |
| # Mission section | |
| with st.container(): | |
| st.markdown("## 🎯 Our Mission") | |
| st.markdown(""" | |
| <div class="custom-card"> | |
| <p style="font-size: 1.1rem; line-height: 1.6; color: #444;"> | |
| To make mathematics accessible, engaging, and fun for students of all levels through | |
| interactive Olympiad-style training. We believe every student can excel in mathematics | |
| with the right guidance and practice. | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Features section | |
| st.markdown("## ✨ Key Features") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown( | |
| """ | |
| <div class="custom-card"> | |
| <h4 style="color: #667eea;">🤖 AI-Powered Learning</h4> | |
| <ul style="color: #444;"> | |
| <li><strong>Unique Questions:</strong> Never see the same question twice</li> | |
| <li><strong>Smart Difficulty:</strong> Automatic adjustment based on performance</li> | |
| <li><strong>Text-Only Focus:</strong> Pure mathematical reasoning, no visual puzzles</li> | |
| </ul> | |
| <h4 style="color: #667eea;">🎮 Multiple Game Modes</h4> | |
| <ul style="color: #444;"> | |
| <li><strong>Practice Mode:</strong> Learn at your own pace</li> | |
| <li><strong>Timed Mode:</strong> 1 minute per question challenge</li> | |
| <li><strong>Challenge Mode:</strong> Push your limits with hard problems</li> | |
| </ul> | |
| </div> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| with col2: | |
| st.markdown( | |
| """ | |
| <div class="custom-card"> | |
| <h4 style="color: #667eea;">📊 Progress & Analytics</h4> | |
| <ul style="color: #444;"> | |
| <li><strong>Performance Tracking:</strong> Monitor improvement over time</li> | |
| <li><strong>Detailed Insights:</strong> Bullet-point feedback with emojis</li> | |
| <li><strong>Visual Charts:</strong> Interactive graphs and analysis</li> | |
| </ul> | |
| <h4 style="color: #667eea;">🏆 Achievement System</h4> | |
| <ul style="color: #444;"> | |
| <li><strong>Badges & Rewards:</strong> Celebrate your success</li> | |
| <li><strong>Streak Tracking:</strong> Build consistent study habits</li> | |
| <li><strong>Level Progression:</strong> Unlock new challenges</li> | |
| </ul> | |
| </div> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| # Getting Started Guide | |
| st.markdown("## 🚀 Getting Started") | |
| steps_col1, steps_col2 = st.columns(2) | |
| with steps_col1: | |
| st.markdown(""" | |
| <div class="custom-card"> | |
| <h4 style="color: #667eea;">📋 Setup Steps</h4> | |
| <ol style="color: #444;"> | |
| <li><strong>Select Grade Level:</strong> Choose from Grade 1-10</li> | |
| <li><strong>Pick Exam Mode:</strong> Practice, Timed, or Challenge</li> | |
| <li><strong>Set Difficulty:</strong> Easy, Medium, or Hard</li> | |
| <li><strong>Choose Questions:</strong> 3-20 questions per exam</li> | |
| <li><strong>Generate Exam:</strong> Click the generate button</li> | |
| </ol> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with steps_col2: | |
| st.markdown(""" | |
| <div class="custom-card"> | |
| <h4 style="color: #667eea;">💡 Success Tips</h4> | |
| <ul style="color: #444;"> | |
| <li><strong>Start Easy:</strong> Begin with Practice mode</li> | |
| <li><strong>Use Hints:</strong> Don't hesitate to ask for help</li> | |
| <li><strong>Review Mistakes:</strong> Learn from wrong answers</li> | |
| <li><strong>Track Progress:</strong> Check your Dashboard regularly</li> | |
| <li><strong>Stay Consistent:</strong> Practice a little each day</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Grade Level Guide | |
| st.markdown("## 🎯 Grade Level Guide") | |
| grade_col1, grade_col2, grade_col3 = st.columns(3) | |
| with grade_col1: | |
| st.markdown(""" | |
| <div class="custom-card"> | |
| <h4 style="color: #667eea;">🌱 Elementary (Grades 1-4)</h4> | |
| <ul style="color: #444;"> | |
| <li>Basic arithmetic operations</li> | |
| <li>Simple word problems</li> | |
| <li>Number patterns and sequences</li> | |
| <li>Basic geometry concepts</li> | |
| <li>Counting and number sense</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with grade_col2: | |
| st.markdown(""" | |
| <div class="custom-card"> | |
| <h4 style="color: #667eea;">🚀 Middle School (Grades 5-8)</h4> | |
| <ul style="color: #444;"> | |
| <li>Pre-algebra concepts</li> | |
| <li>Fractions and decimals</li> | |
| <li>Basic statistics</li> | |
| <li>Coordinate geometry</li> | |
| <li>Problem-solving strategies</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with grade_col3: | |
| st.markdown(""" | |
| <div class="custom-card"> | |
| <h4 style="color: #667eea;">⭐ High School (Grades 9-10)</h4> | |
| <ul style="color: #444;"> | |
| <li>Advanced algebra</li> | |
| <li>Geometry proofs</li> | |
| <li>Trigonometry basics</li> | |
| <li>Complex problem solving</li> | |
| <li>Mathematical reasoning</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # FAQ Section | |
| with st.expander("❓ Frequently Asked Questions"): | |
| st.markdown(""" | |
| **Q: How are questions made unique?** | |
| A: We use advanced AI with hash tracking to ensure you never see the same question twice, even across different sessions. | |
| **Q: Why text-based questions only?** | |
| A: We focus purely on mathematical reasoning and calculations, avoiding visual puzzles that don't translate well to text interfaces. | |
| **Q: How does the timer work in timed mode?** | |
| A: You get exactly 1 minute per question. For 5 questions, you get 5 minutes total. The timer is prominently displayed above the questions. | |
| **Q: What if I get stuck on a question?** | |
| A: Use the hint feature! We provide helpful hints to guide your thinking without giving away the answer. | |
| **Q: How is my progress tracked?** | |
| A: We track your percentage, response times, difficulty preferences, and improvement trends with detailed analytics. | |
| **Q: How do I earn badges?** | |
| A: Badges are earned automatically based on your performance, consistency, and achievements. | |
| """) | |
| # Footer | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style="text-align: center; padding: 2rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20px; color: white;"> | |
| <h3 style="color: white; margin-bottom: 1rem;">Ready to become a Math Genius? 🧮</h3> | |
| <p style="color: rgba(255,255,255,0.9); margin: 0;"> | |
| Join thousands of students improving their mathematical skills with unique, challenging questions every day! | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown("**Made with ❤️ for young mathematicians everywhere!**") | |
| if __name__ == "__main__": | |
| main() | |