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("""
""", 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()
@st.cache_resource
def get_exam_system():
return MathOlympiadExam()
def render_hero_section():
"""Render the hero section with animations"""
st.markdown("""
""", 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"""
â°
{mins:02d}:{secs:02d}
Time Remaining
"""
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"""
{profile['total_exams']}
Exams Taken
""", unsafe_allow_html=True)
with col2:
percentage = (profile['correct_answers'] / max(profile['total_questions'], 1)) * 100
st.markdown(f"""
{percentage:.1f}%
percentage
""", unsafe_allow_html=True)
with col3:
st.markdown(f"""
{profile['streak']}
Day Streak
""", unsafe_allow_html=True)
with col4:
st.markdown(f"""
{len(profile['badges'])}
Badges Earned
""", 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"""
Question {question_index + 1} of {total_questions}
""", unsafe_allow_html=True)
st.markdown(f"""
Question {question['question_number']}
{question.get('difficulty', 'Medium')} Level
{question['question_text']}
""", 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"""
{correct_count}/{total_questions}
Final Score
""", unsafe_allow_html=True)
with col2:
st.markdown(f"""
{percentage:.1f}%
percentage
""", 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"""
{grade_emoji}
{grade_text}
""", 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"""
{summary}
""", 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"""
đ
{st.session_state.user_profile['name']}
Level {st.session_state.user_profile['favorite_grade']} Explorer
""", 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("""
đ
Welcome to MathGenius Academy!
Ready to embark on an exciting mathematical journey? Let's discover your potential!
""", 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("""
đ¯
Unique Questions
Always new, never repeated
""", unsafe_allow_html=True)
with col2:
st.markdown("""
đ
Text-Based Only
Pure math, no visual puzzles
""", unsafe_allow_html=True)
with col3:
st.markdown("""
â°
Smart Timer
1 minute per question
""", 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("""
"Mathematics is not about numbers, equations, or algorithms. It is about understanding." - William Paul Thurston
""", 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"""
{badge.split()[0]}
{' '.join(badge.split()[1:])}
""", unsafe_allow_html=True)
with tab3:
st.markdown("# đ About MathGenius Academy")
# Mission section
with st.container():
st.markdown("## đ¯ Our Mission")
st.markdown("""
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.
""", unsafe_allow_html=True)
# Features section
st.markdown("## ⨠Key Features")
col1, col2 = st.columns(2)
with col1:
st.markdown(
"""
đ¤ AI-Powered Learning
- Unique Questions: Never see the same question twice
- Smart Difficulty: Automatic adjustment based on performance
- Text-Only Focus: Pure mathematical reasoning, no visual puzzles
đŽ Multiple Game Modes
- Practice Mode: Learn at your own pace
- Timed Mode: 1 minute per question challenge
- Challenge Mode: Push your limits with hard problems
""",
unsafe_allow_html=True
)
with col2:
st.markdown(
"""
đ Progress & Analytics
- Performance Tracking: Monitor improvement over time
- Detailed Insights: Bullet-point feedback with emojis
- Visual Charts: Interactive graphs and analysis
đ Achievement System
- Badges & Rewards: Celebrate your success
- Streak Tracking: Build consistent study habits
- Level Progression: Unlock new challenges
""",
unsafe_allow_html=True
)
# Getting Started Guide
st.markdown("## đ Getting Started")
steps_col1, steps_col2 = st.columns(2)
with steps_col1:
st.markdown("""
đ Setup Steps
- Select Grade Level: Choose from Grade 1-10
- Pick Exam Mode: Practice, Timed, or Challenge
- Set Difficulty: Easy, Medium, or Hard
- Choose Questions: 3-20 questions per exam
- Generate Exam: Click the generate button
""", unsafe_allow_html=True)
with steps_col2:
st.markdown("""
đĄ Success Tips
- Start Easy: Begin with Practice mode
- Use Hints: Don't hesitate to ask for help
- Review Mistakes: Learn from wrong answers
- Track Progress: Check your Dashboard regularly
- Stay Consistent: Practice a little each day
""", 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("""
đą Elementary (Grades 1-4)
- Basic arithmetic operations
- Simple word problems
- Number patterns and sequences
- Basic geometry concepts
- Counting and number sense
""", unsafe_allow_html=True)
with grade_col2:
st.markdown("""
đ Middle School (Grades 5-8)
- Pre-algebra concepts
- Fractions and decimals
- Basic statistics
- Coordinate geometry
- Problem-solving strategies
""", unsafe_allow_html=True)
with grade_col3:
st.markdown("""
â High School (Grades 9-10)
- Advanced algebra
- Geometry proofs
- Trigonometry basics
- Complex problem solving
- Mathematical reasoning
""", 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("""
Ready to become a Math Genius? đ§Ž
Join thousands of students improving their mathematical skills with unique, challenging questions every day!
""", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
st.markdown("**Made with â¤ī¸ for young mathematicians everywhere!**")
if __name__ == "__main__":
main()