Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +437 -175
src/streamlit_app.py
CHANGED
|
@@ -10,6 +10,7 @@ import plotly.graph_objects as go
|
|
| 10 |
import pandas as pd
|
| 11 |
from datetime import datetime, timedelta
|
| 12 |
import streamlit.components.v1 as components
|
|
|
|
| 13 |
|
| 14 |
# Configure page
|
| 15 |
st.set_page_config(
|
|
@@ -77,6 +78,37 @@ def load_custom_css():
|
|
| 77 |
z-index: 1;
|
| 78 |
}
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
/* Card Styling */
|
| 81 |
.custom-card {
|
| 82 |
background: white;
|
|
@@ -222,12 +254,6 @@ def load_custom_css():
|
|
| 222 |
animation: slideInUp 0.6s ease-out;
|
| 223 |
}
|
| 224 |
|
| 225 |
-
@keyframes pulse {
|
| 226 |
-
0% { transform: scale(1); }
|
| 227 |
-
50% { transform: scale(1.05); }
|
| 228 |
-
100% { transform: scale(1); }
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
.pulse-animation {
|
| 232 |
animation: pulse 2s infinite;
|
| 233 |
}
|
|
@@ -270,6 +296,11 @@ def load_custom_css():
|
|
| 270 |
.question-card {
|
| 271 |
padding: 1rem;
|
| 272 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
}
|
| 274 |
body, .main, .block-container {
|
| 275 |
background: #f7f9fb !important;
|
|
@@ -371,106 +402,271 @@ class MathOlympiadExam:
|
|
| 371 |
return None
|
| 372 |
|
| 373 |
def generate_questions(self, grade: int, num_questions: int, difficulty: str = "medium") -> List[Dict]:
|
| 374 |
-
"""Generate math olympiad questions with
|
| 375 |
if not self.client:
|
| 376 |
return []
|
| 377 |
|
| 378 |
try:
|
| 379 |
difficulty_map = {
|
| 380 |
-
1: "
|
| 381 |
-
2: "
|
| 382 |
-
3: "
|
| 383 |
-
4: "
|
| 384 |
-
5: "
|
| 385 |
-
6: "
|
| 386 |
-
7: "
|
| 387 |
-
8: "
|
| 388 |
-
9: "
|
| 389 |
-
10: "
|
| 390 |
}
|
| 391 |
-
|
| 392 |
difficulty_modifiers = {
|
| 393 |
-
"easy": "
|
| 394 |
-
"medium": "Standard olympiad difficulty with moderate complexity",
|
| 395 |
-
"hard": "Advanced
|
| 396 |
}
|
| 397 |
|
| 398 |
topics = difficulty_map.get(grade, "general math concepts")
|
| 399 |
modifier = difficulty_modifiers.get(difficulty, "")
|
| 400 |
-
|
| 401 |
-
prompt = f"""Generate exactly {num_questions} Math Olympiad-style multiple choice questions for Grade {grade} students.
|
| 402 |
|
| 403 |
-
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
|
| 406 |
-
|
| 407 |
-
**Question X:** [Question text here]
|
| 408 |
-
A) [Option A]
|
| 409 |
-
B) [Option B]
|
| 410 |
-
C) [Option C]
|
| 411 |
-
D) [Option D]
|
| 412 |
-
**Correct Answer:** [Letter only - A, B, C, or D]
|
| 413 |
-
**Explanation:** [Clear step-by-step solution]
|
| 414 |
-
**Hint:** [Helpful hint for students]
|
| 415 |
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
|
| 418 |
-
response = self.client.chat.completions.create(
|
| 419 |
-
model="llama-3.3-70b-versatile",
|
| 420 |
-
messages=[
|
| 421 |
-
{"role": "system", "content": "You are an expert Math Olympiad coach creating engaging, educational questions."},
|
| 422 |
-
{"role": "user", "content": prompt}
|
| 423 |
-
],
|
| 424 |
-
temperature=0.7,
|
| 425 |
-
max_tokens=3000
|
| 426 |
-
)
|
| 427 |
-
|
| 428 |
-
return self._parse_questions(response.choices[0].message.content)
|
| 429 |
-
|
| 430 |
except Exception as e:
|
| 431 |
st.error(f"Failed to generate questions: {e}")
|
| 432 |
return []
|
| 433 |
|
| 434 |
-
def
|
| 435 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
questions = []
|
| 437 |
-
question_blocks = re.split(r'\*\*Question \d+:\*\*', content)[1:]
|
| 438 |
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
try:
|
| 441 |
lines = [line.strip() for line in block.strip().split('\n') if line.strip()]
|
| 442 |
|
| 443 |
-
if len(lines) < 6:
|
| 444 |
continue
|
| 445 |
|
| 446 |
-
question_text = lines[0]
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
correct_answer = None
|
| 449 |
explanation = ""
|
| 450 |
hint = ""
|
| 451 |
|
|
|
|
|
|
|
| 452 |
for line in lines[1:]:
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
elif line.startswith('**Explanation:**'):
|
| 458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
elif line.startswith('**Hint:**'):
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
|
| 462 |
-
|
|
|
|
| 463 |
questions.append({
|
| 464 |
'question_number': i,
|
| 465 |
'question_text': question_text,
|
| 466 |
-
'options':
|
|
|
|
| 467 |
'correct_answer': correct_answer,
|
| 468 |
-
'explanation': explanation,
|
| 469 |
-
'hint': hint,
|
| 470 |
'user_answer': None,
|
| 471 |
'time_taken': 0,
|
| 472 |
'difficulty': self._estimate_difficulty(question_text)
|
| 473 |
})
|
|
|
|
|
|
|
| 474 |
|
| 475 |
except Exception as e:
|
| 476 |
st.warning(f"Error parsing question {i}: {e}")
|
|
@@ -480,8 +676,8 @@ Make questions engaging and educational. Include varied problem types: word prob
|
|
| 480 |
|
| 481 |
def _estimate_difficulty(self, question_text: str) -> str:
|
| 482 |
"""Estimate question difficulty based on content"""
|
| 483 |
-
hard_keywords = ['
|
| 484 |
-
easy_keywords = ['
|
| 485 |
|
| 486 |
text_lower = question_text.lower()
|
| 487 |
|
|
@@ -518,8 +714,8 @@ def initialize_session_state():
|
|
| 518 |
'badges': [],
|
| 519 |
'streak': 0
|
| 520 |
}
|
| 521 |
-
if '
|
| 522 |
-
st.session_state.
|
| 523 |
|
| 524 |
@st.cache_resource
|
| 525 |
def get_exam_system():
|
|
@@ -532,12 +728,49 @@ def render_hero_section():
|
|
| 532 |
<div class="hero-title">๐งฎ MathGenius Academy</div>
|
| 533 |
<div class="hero-subtitle">Master Mathematics Through Interactive Olympiad Training</div>
|
| 534 |
<div style="margin-top: 1rem;">
|
| 535 |
-
<span style="background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 25px; margin: 0 0.5rem;">๐ฏ
|
| 536 |
<span style="background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 25px; margin: 0 0.5rem;">๐ Progress Tracking</span>
|
| 537 |
<span style="background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 25px; margin: 0 0.5rem;">๐ Achievement System</span>
|
| 538 |
</div>
|
| 539 |
</div>
|
| 540 |
-
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
|
| 542 |
def render_dashboard():
|
| 543 |
"""Render user dashboard with statistics"""
|
|
@@ -596,21 +829,9 @@ def render_dashboard():
|
|
| 596 |
paper_bgcolor='rgba(0,0,0,0)',
|
| 597 |
)
|
| 598 |
st.plotly_chart(fig, use_container_width=True)
|
| 599 |
-
|
| 600 |
-
with col2:
|
| 601 |
-
st.markdown("#### ๐ฏ Grade Distribution")
|
| 602 |
-
grade_counts = df['grade'].value_counts().sort_index()
|
| 603 |
-
fig = px.bar(x=grade_counts.index, y=grade_counts.values,
|
| 604 |
-
title='Exams by Grade Level',
|
| 605 |
-
color_discrete_sequence=['#764ba2'])
|
| 606 |
-
fig.update_layout(
|
| 607 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 608 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 609 |
-
)
|
| 610 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 611 |
|
| 612 |
def render_question_card(question, question_index, total_questions):
|
| 613 |
-
"""Render an individual question with enhanced UI"""
|
| 614 |
progress = ((question_index + 1) / total_questions) * 100
|
| 615 |
|
| 616 |
st.markdown(f"""
|
|
@@ -635,17 +856,51 @@ def render_question_card(question, question_index, total_questions):
|
|
| 635 |
</div>
|
| 636 |
""", unsafe_allow_html=True)
|
| 637 |
|
| 638 |
-
#
|
| 639 |
-
|
| 640 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
|
| 642 |
-
|
| 643 |
-
"
|
| 644 |
-
|
| 645 |
-
format_func=lambda x, labels=option_labels, keys=option_keys: f"{x}) {labels[keys.index(x)]}",
|
| 646 |
-
key=f"q_{question_index}",
|
| 647 |
-
index=None
|
| 648 |
-
)
|
| 649 |
|
| 650 |
# Hint system
|
| 651 |
if st.button(f"๐ก Need a hint?", key=f"hint_{question_index}"):
|
|
@@ -657,30 +912,12 @@ def render_question_card(question, question_index, total_questions):
|
|
| 657 |
return selected
|
| 658 |
|
| 659 |
def render_exam_interface():
|
| 660 |
-
"""Render the main exam interface"""
|
| 661 |
questions = st.session_state.questions
|
| 662 |
total_questions = len(questions)
|
| 663 |
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
if st.session_state.start_time:
|
| 667 |
-
elapsed = time.time() - st.session_state.start_time
|
| 668 |
-
remaining = max(0, (total_questions * 120) - elapsed) # 2 minutes per question
|
| 669 |
-
|
| 670 |
-
mins, secs = divmod(int(remaining), 60)
|
| 671 |
-
st.markdown(f"""
|
| 672 |
-
<div style="text-align: center; margin-bottom: 1rem;">
|
| 673 |
-
<div style="background: linear-gradient(135deg, #FF6B6B, #FF8E8E); color: white;
|
| 674 |
-
padding: 1rem; border-radius: 15px; display: inline-block; font-size: 1.5rem; font-weight: 600;">
|
| 675 |
-
โฐ {mins:02d}:{secs:02d}
|
| 676 |
-
</div>
|
| 677 |
-
</div>
|
| 678 |
-
""", unsafe_allow_html=True)
|
| 679 |
-
|
| 680 |
-
if remaining <= 0:
|
| 681 |
-
st.error("โฐ Time's up! Submitting your exam...")
|
| 682 |
-
st.session_state.exam_completed = True
|
| 683 |
-
st.rerun()
|
| 684 |
|
| 685 |
# Question navigation
|
| 686 |
if total_questions > 1:
|
|
@@ -695,6 +932,7 @@ def render_exam_interface():
|
|
| 695 |
# Current question
|
| 696 |
current_q = st.session_state.current_question
|
| 697 |
if current_q < total_questions:
|
|
|
|
| 698 |
selected = render_question_card(questions[current_q], current_q, total_questions)
|
| 699 |
questions[current_q]['user_answer'] = selected
|
| 700 |
|
|
@@ -742,7 +980,7 @@ def calculate_badges(score, total, grade):
|
|
| 742 |
return badges
|
| 743 |
|
| 744 |
def render_results_section():
|
| 745 |
-
"""Render enhanced results with animations and insights
|
| 746 |
st.markdown("---")
|
| 747 |
st.markdown("### ๐ Exam Results")
|
| 748 |
|
|
@@ -835,7 +1073,7 @@ def render_results_section():
|
|
| 835 |
if not is_correct and question.get('hint'):
|
| 836 |
st.info(f"๐ก **Hint for next time:** {question['hint']}")
|
| 837 |
|
| 838 |
-
# Performance insights
|
| 839 |
exam_system = get_exam_system()
|
| 840 |
if exam_system.client:
|
| 841 |
st.markdown("#### ๐ง AI Performance Analysis")
|
|
@@ -854,12 +1092,12 @@ def render_results_section():
|
|
| 854 |
</div>
|
| 855 |
""", unsafe_allow_html=True)
|
| 856 |
|
| 857 |
-
# Show completion message
|
| 858 |
st.markdown("#### ๐ Exam Complete!")
|
| 859 |
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.")
|
| 860 |
|
| 861 |
def generate_enhanced_summary(grade, questions, score, total, exam_system):
|
| 862 |
-
"""Generate enhanced AI-powered performance summary"""
|
| 863 |
try:
|
| 864 |
# Analyze performance patterns
|
| 865 |
correct_questions = [q for q in questions if q['user_answer'] == q['correct_answer']]
|
|
@@ -885,19 +1123,32 @@ PERFORMANCE SUMMARY:
|
|
| 885 |
SAMPLE INCORRECT QUESTIONS:
|
| 886 |
{chr(10).join([f"- {q['question_text'][:100]}..." for q in incorrect_questions[:2]])}
|
| 887 |
|
| 888 |
-
Please provide a
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 893 |
|
| 894 |
response = exam_system.client.chat.completions.create(
|
| 895 |
-
model="
|
| 896 |
messages=[
|
| 897 |
-
{"role": "system", "content": "You are an expert Math Olympiad mentor providing detailed, encouraging feedback
|
| 898 |
{"role": "user", "content": prompt}
|
| 899 |
],
|
| 900 |
-
temperature=0
|
| 901 |
max_tokens=1000
|
| 902 |
)
|
| 903 |
|
|
@@ -905,9 +1156,20 @@ Use simple, encouraging language for a Grade {grade} student. Avoid long lists o
|
|
| 905 |
|
| 906 |
except Exception as e:
|
| 907 |
return f"""
|
| 908 |
-
**
|
| 909 |
-
You
|
| 910 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
"""
|
| 912 |
|
| 913 |
def reset_exam():
|
|
@@ -953,10 +1215,10 @@ def render_sidebar():
|
|
| 953 |
["practice", "timed", "challenge"],
|
| 954 |
format_func=lambda x: {
|
| 955 |
"practice": "๐ฏ Practice Mode",
|
| 956 |
-
"timed": "โฐ Timed Mode",
|
| 957 |
"challenge": "๐ฅ Challenge Mode"
|
| 958 |
}[x],
|
| 959 |
-
help="Practice: No time limit | Timed:
|
| 960 |
)
|
| 961 |
st.session_state.exam_mode = exam_mode
|
| 962 |
|
|
@@ -966,6 +1228,10 @@ def render_sidebar():
|
|
| 966 |
else:
|
| 967 |
num_questions = st.slider("๐ Questions:", 3, 15, 5, help="Choose number of questions")
|
| 968 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 969 |
# Difficulty level
|
| 970 |
difficulty = st.select_slider(
|
| 971 |
"โก Difficulty:",
|
|
@@ -983,7 +1249,7 @@ def render_sidebar():
|
|
| 983 |
if not exam_system.client:
|
| 984 |
st.error("๐ซ API not available!")
|
| 985 |
else:
|
| 986 |
-
with st.spinner(f"๐ญ Creating your {exam_mode} exam..."):
|
| 987 |
# Add loading animation
|
| 988 |
progress_bar = st.progress(0)
|
| 989 |
for i in range(100):
|
|
@@ -994,21 +1260,13 @@ def render_sidebar():
|
|
| 994 |
questions = exam_system.generate_questions(grade, num_questions, challenge_difficulty)
|
| 995 |
|
| 996 |
if questions:
|
| 997 |
-
|
| 998 |
-
if 'seen_questions' in st.session_state:
|
| 999 |
-
new_questions = [q for q in questions if q['question_text'] not in st.session_state.seen_questions]
|
| 1000 |
-
st.session_state.questions = new_questions
|
| 1001 |
-
# Update seen_questions with the new questions
|
| 1002 |
-
st.session_state.seen_questions.update(q['question_text'] for q in new_questions)
|
| 1003 |
-
else:
|
| 1004 |
-
st.session_state.questions = questions
|
| 1005 |
-
|
| 1006 |
st.session_state.exam_completed = False
|
| 1007 |
st.session_state.grade_selected = grade
|
| 1008 |
st.session_state.current_question = 0
|
| 1009 |
st.session_state.start_time = time.time() if exam_mode == "timed" else None
|
| 1010 |
|
| 1011 |
-
st.success(f"โจ Generated {len(st.session_state.questions)} questions!")
|
| 1012 |
st.balloons()
|
| 1013 |
else:
|
| 1014 |
st.error("โ Failed to generate exam. Try again!")
|
|
@@ -1068,26 +1326,26 @@ def render_welcome_screen():
|
|
| 1068 |
st.markdown("""
|
| 1069 |
<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;">
|
| 1070 |
<div style="font-size: 3rem; margin-bottom: 1rem;">๐ฏ</div>
|
| 1071 |
-
<h4 style="margin: 0.5rem 0; color: white;">
|
| 1072 |
-
<p style="margin: 0; color: rgba(255,255,255,0.9);">
|
| 1073 |
</div>
|
| 1074 |
""", unsafe_allow_html=True)
|
| 1075 |
|
| 1076 |
with col2:
|
| 1077 |
st.markdown("""
|
| 1078 |
<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;">
|
| 1079 |
-
<div style="font-size: 3rem; margin-bottom: 1rem;"
|
| 1080 |
-
<h4 style="margin: 0.5rem 0; color: white;">
|
| 1081 |
-
<p style="margin: 0; color: rgba(255,255,255,0.9);">
|
| 1082 |
</div>
|
| 1083 |
""", unsafe_allow_html=True)
|
| 1084 |
|
| 1085 |
with col3:
|
| 1086 |
st.markdown("""
|
| 1087 |
<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;">
|
| 1088 |
-
<div style="font-size: 3rem; margin-bottom: 1rem;"
|
| 1089 |
-
<h4 style="margin: 0.5rem 0; color: white;">
|
| 1090 |
-
<p style="margin: 0; color: rgba(255,255,255,0.9);">
|
| 1091 |
</div>
|
| 1092 |
""", unsafe_allow_html=True)
|
| 1093 |
|
|
@@ -1103,7 +1361,7 @@ def render_welcome_screen():
|
|
| 1103 |
1. **๐ฏ Choose Your Grade**: Select your current grade level from the sidebar
|
| 1104 |
2. **๐ Pick Your Mode**:
|
| 1105 |
- ๐ข **Practice Mode**: No time pressure, perfect for learning
|
| 1106 |
-
- โฐ **Timed Mode**:
|
| 1107 |
- ๐ฅ **Challenge Mode**: Extra difficult questions for advanced learners
|
| 1108 |
3. **โก Set Difficulty**: Easy, Medium, or Hard - pick what feels right
|
| 1109 |
4. **๐ Choose Questions**: 3-20 questions depending on your time
|
|
@@ -1151,15 +1409,18 @@ def main():
|
|
| 1151 |
else:
|
| 1152 |
st.markdown("### ๐งฎ Math Olympiad Challenge")
|
| 1153 |
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
|
|
|
|
|
|
|
|
|
| 1160 |
st.info(f"โฑ๏ธ {st.session_state.exam_mode.title()} Mode")
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
|
| 1164 |
render_exam_interface()
|
| 1165 |
|
|
@@ -1243,14 +1504,14 @@ def main():
|
|
| 1243 |
<div class="custom-card">
|
| 1244 |
<h4 style="color: #667eea;">๐ค AI-Powered Learning</h4>
|
| 1245 |
<ul style="color: #444;">
|
| 1246 |
-
<li><strong>
|
| 1247 |
<li><strong>Smart Difficulty:</strong> Automatic adjustment based on performance</li>
|
| 1248 |
-
<li><strong>
|
| 1249 |
</ul>
|
| 1250 |
<h4 style="color: #667eea;">๐ฎ Multiple Game Modes</h4>
|
| 1251 |
<ul style="color: #444;">
|
| 1252 |
<li><strong>Practice Mode:</strong> Learn at your own pace</li>
|
| 1253 |
-
<li><strong>Timed Mode:</strong>
|
| 1254 |
<li><strong>Challenge Mode:</strong> Push your limits with hard problems</li>
|
| 1255 |
</ul>
|
| 1256 |
</div>
|
|
@@ -1265,7 +1526,7 @@ def main():
|
|
| 1265 |
<h4 style="color: #667eea;">๐ Progress & Analytics</h4>
|
| 1266 |
<ul style="color: #444;">
|
| 1267 |
<li><strong>Performance Tracking:</strong> Monitor improvement over time</li>
|
| 1268 |
-
<li><strong>Detailed
|
| 1269 |
<li><strong>Visual Charts:</strong> Interactive graphs and analysis</li>
|
| 1270 |
</ul>
|
| 1271 |
<h4 style="color: #667eea;">๐ Achievement System</h4>
|
|
@@ -1324,8 +1585,8 @@ def main():
|
|
| 1324 |
<ul style="color: #444;">
|
| 1325 |
<li>Basic arithmetic operations</li>
|
| 1326 |
<li>Simple word problems</li>
|
| 1327 |
-
<li>
|
| 1328 |
-
<li>Basic geometry
|
| 1329 |
<li>Counting and number sense</li>
|
| 1330 |
</ul>
|
| 1331 |
</div>
|
|
@@ -1362,25 +1623,25 @@ def main():
|
|
| 1362 |
# FAQ Section
|
| 1363 |
with st.expander("โ Frequently Asked Questions"):
|
| 1364 |
st.markdown("""
|
| 1365 |
-
**Q: How are
|
| 1366 |
|
| 1367 |
-
A: We use advanced AI to
|
| 1368 |
|
| 1369 |
-
**Q:
|
| 1370 |
|
| 1371 |
-
A:
|
| 1372 |
|
| 1373 |
-
**Q: How
|
| 1374 |
|
| 1375 |
-
A:
|
| 1376 |
|
| 1377 |
**Q: What if I get stuck on a question?**
|
| 1378 |
|
| 1379 |
A: Use the hint feature! We provide helpful hints to guide your thinking without giving away the answer.
|
| 1380 |
|
| 1381 |
-
**Q:
|
| 1382 |
|
| 1383 |
-
A:
|
| 1384 |
|
| 1385 |
**Q: How do I earn badges?**
|
| 1386 |
|
|
@@ -1393,7 +1654,7 @@ def main():
|
|
| 1393 |
<div style="text-align: center; padding: 2rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20px; color: white;">
|
| 1394 |
<h3 style="color: white; margin-bottom: 1rem;">Ready to become a Math Genius? ๐งฎ</h3>
|
| 1395 |
<p style="color: rgba(255,255,255,0.9); margin: 0;">
|
| 1396 |
-
Join thousands of students improving their mathematical skills every day!
|
| 1397 |
</p>
|
| 1398 |
</div>
|
| 1399 |
""", unsafe_allow_html=True)
|
|
@@ -1402,4 +1663,5 @@ def main():
|
|
| 1402 |
st.markdown("**Made with โค๏ธ for young mathematicians everywhere!**")
|
| 1403 |
|
| 1404 |
if __name__ == "__main__":
|
| 1405 |
-
main()
|
|
|
|
|
|
| 10 |
import pandas as pd
|
| 11 |
from datetime import datetime, timedelta
|
| 12 |
import streamlit.components.v1 as components
|
| 13 |
+
import hashlib
|
| 14 |
|
| 15 |
# Configure page
|
| 16 |
st.set_page_config(
|
|
|
|
| 78 |
z-index: 1;
|
| 79 |
}
|
| 80 |
|
| 81 |
+
/* Timer styling */
|
| 82 |
+
.timer-display {
|
| 83 |
+
background: linear-gradient(135deg, #FF6B6B, #FF8E8E);
|
| 84 |
+
color: white;
|
| 85 |
+
padding: 1rem 2rem;
|
| 86 |
+
border-radius: 20px;
|
| 87 |
+
text-align: center;
|
| 88 |
+
font-size: 2rem;
|
| 89 |
+
font-weight: 700;
|
| 90 |
+
margin-bottom: 2rem;
|
| 91 |
+
box-shadow: 0 10px 30px rgba(255, 107, 107, 0.3);
|
| 92 |
+
animation: pulse 2s infinite;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
@keyframes pulse {
|
| 96 |
+
0% { transform: scale(1); box-shadow: 0 10px 30px rgba(255, 107, 107, 0.3); }
|
| 97 |
+
50% { transform: scale(1.02); box-shadow: 0 15px 40px rgba(255, 107, 107, 0.5); }
|
| 98 |
+
100% { transform: scale(1); box-shadow: 0 10px 30px rgba(255, 107, 107, 0.3); }
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.timer-warning {
|
| 102 |
+
background: linear-gradient(135deg, #FF4757, #FF6B6B) !important;
|
| 103 |
+
animation: fastPulse 1s infinite !important;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
@keyframes fastPulse {
|
| 107 |
+
0% { transform: scale(1); }
|
| 108 |
+
50% { transform: scale(1.05); }
|
| 109 |
+
100% { transform: scale(1); }
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
/* Card Styling */
|
| 113 |
.custom-card {
|
| 114 |
background: white;
|
|
|
|
| 254 |
animation: slideInUp 0.6s ease-out;
|
| 255 |
}
|
| 256 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
.pulse-animation {
|
| 258 |
animation: pulse 2s infinite;
|
| 259 |
}
|
|
|
|
| 296 |
.question-card {
|
| 297 |
padding: 1rem;
|
| 298 |
}
|
| 299 |
+
|
| 300 |
+
.timer-display {
|
| 301 |
+
font-size: 1.5rem;
|
| 302 |
+
padding: 0.75rem 1.5rem;
|
| 303 |
+
}
|
| 304 |
}
|
| 305 |
body, .main, .block-container {
|
| 306 |
background: #f7f9fb !important;
|
|
|
|
| 402 |
return None
|
| 403 |
|
| 404 |
def generate_questions(self, grade: int, num_questions: int, difficulty: str = "medium") -> List[Dict]:
|
| 405 |
+
"""Generate unique math olympiad questions with STRICT rules against visual questions - FIXED"""
|
| 406 |
if not self.client:
|
| 407 |
return []
|
| 408 |
|
| 409 |
try:
|
| 410 |
difficulty_map = {
|
| 411 |
+
1: "Basic counting 1-20, simple addition/subtraction within 20, comparing numbers, basic money problems",
|
| 412 |
+
2: "Addition/subtraction within 100, word problems about time/money, simple multiplication by 2,5,10",
|
| 413 |
+
3: "Multiplication tables to 12, basic division, simple fractions (1/2, 1/4), word problems with multiple steps",
|
| 414 |
+
4: "Multi-digit multiplication/division, equivalent fractions, decimals to tenths, perimeter calculations",
|
| 415 |
+
5: "Advanced arithmetic, basic algebra (find the missing number), percentages, area calculations, factors/multiples",
|
| 416 |
+
6: "Pre-algebra equations, ratios/proportions, basic statistics (mean/median), coordinate problems",
|
| 417 |
+
7: "Linear equations, integers, basic probability, algebraic expressions, geometric formulas",
|
| 418 |
+
8: "Systems of equations, exponents/roots, advanced probability, quadratic expressions, geometric theorems",
|
| 419 |
+
9: "Advanced algebra, trigonometry basics, sequences/series, complex word problems, mathematical proofs",
|
| 420 |
+
10: "Pre-calculus topics, advanced trigonometry, logarithms, complex problem solving, advanced geometry"
|
| 421 |
}
|
| 422 |
+
|
| 423 |
difficulty_modifiers = {
|
| 424 |
+
"easy": "Focus on fundamental concepts with straightforward calculations",
|
| 425 |
+
"medium": "Standard olympiad difficulty with moderate complexity requiring 2-3 steps",
|
| 426 |
+
"hard": "Advanced multi-step problems requiring deep mathematical reasoning"
|
| 427 |
}
|
| 428 |
|
| 429 |
topics = difficulty_map.get(grade, "general math concepts")
|
| 430 |
modifier = difficulty_modifiers.get(difficulty, "")
|
|
|
|
|
|
|
| 431 |
|
| 432 |
+
# STRICT forbidden keywords - expanded list
|
| 433 |
+
forbidden_keywords = [
|
| 434 |
+
"figure", "diagram", "shape", "block", "square", "triangle", "circle", "rectangle", "polygon",
|
| 435 |
+
"pattern below", "picture", "image", "graph", "chart", "visual", "draw", "sketch", "plot",
|
| 436 |
+
"look at", "observe", "see the", "shown", "displayed", "illustrated", "appears",
|
| 437 |
+
"geometric shape", "arrange", "tessellation", "symmetry", "reflection", "rotation",
|
| 438 |
+
"hexagon", "pentagon", "octagon", "parallelogram", "rhombus", "trapezoid",
|
| 439 |
+
"coordinate plane", "grid", "lattice", "dot", "point on", "line segment",
|
| 440 |
+
"angle", "vertex", "side", "face", "edge", "cube", "cylinder", "sphere",
|
| 441 |
+
"shaded", "colored", "pattern", "sequence of shapes", "visual pattern"
|
| 442 |
+
]
|
| 443 |
|
| 444 |
+
seen_hashes = getattr(st.session_state, 'question_hashes', set())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
|
| 446 |
+
# ENHANCED prompt with VERY strict formatting requirements
|
| 447 |
+
base_prompt = f"""
|
| 448 |
+
Generate exactly {num_questions} UNIQUE Math Olympiad questions for Grade {grade}.
|
| 449 |
+
|
| 450 |
+
๐จ ABSOLUTE RESTRICTIONS - NEVER INCLUDE:
|
| 451 |
+
โ Any visual elements (shapes, figures, diagrams, patterns, images)
|
| 452 |
+
โ Questions requiring visual interpretation
|
| 453 |
+
โ Geometric shape identification or counting
|
| 454 |
+
โ Pattern completion with visual elements
|
| 455 |
+
โ Any reference to "figure", "diagram", "shape", "pattern below"
|
| 456 |
+
โ Coordinate plotting or graph reading
|
| 457 |
+
โ Any visual arrangement problems
|
| 458 |
+
|
| 459 |
+
โ
ONLY ALLOWED - TEXT-BASED QUESTIONS:
|
| 460 |
+
โ
Pure arithmetic calculations and word problems
|
| 461 |
+
โ
Number sequences (like: 2, 4, 6, 8, ?)
|
| 462 |
+
โ
Logic puzzles with numbers/text only
|
| 463 |
+
โ
Real-world scenarios (money, time, travel, shopping)
|
| 464 |
+
โ
Algebraic word problems
|
| 465 |
+
โ
Statistical problems with given data
|
| 466 |
+
โ
Mathematical reasoning without visuals
|
| 467 |
+
|
| 468 |
+
TOPICS: {topics}
|
| 469 |
+
DIFFICULTY: {modifier}
|
| 470 |
+
|
| 471 |
+
๐ฏ CRITICAL: Answer must be EXACTLY one letter: A, B, C, or D (no explanations in answer line)
|
| 472 |
+
|
| 473 |
+
FORMAT (EXACT - NO DEVIATIONS):
|
| 474 |
+
**Question 1:** [Engaging word problem - NO VISUALS]
|
| 475 |
+
A) [Option A with number/calculation]
|
| 476 |
+
B) [Option B with number/calculation]
|
| 477 |
+
C) [Option C with number/calculation]
|
| 478 |
+
D) [Option D with number/calculation]
|
| 479 |
+
**Answer:** A
|
| 480 |
+
**Explanation:** [Step-by-step solution]
|
| 481 |
+
**Hint:** [Helpful tip for solving]
|
| 482 |
+
|
| 483 |
+
**Question 2:** [Next question...]
|
| 484 |
+
A) [Option A]
|
| 485 |
+
B) [Option B]
|
| 486 |
+
C) [Option C]
|
| 487 |
+
D) [Option D]
|
| 488 |
+
**Answer:** C
|
| 489 |
+
**Explanation:** [Step-by-step solution]
|
| 490 |
+
**Hint:** [Helpful tip for solving]
|
| 491 |
+
|
| 492 |
+
[Continue for all {num_questions} questions]
|
| 493 |
+
|
| 494 |
+
๐ฅ IMPORTANT FORMATTING RULES:
|
| 495 |
+
- Answer line must contain ONLY the letter (A, B, C, or D)
|
| 496 |
+
- No parentheses, explanations, or additional text in the answer
|
| 497 |
+
- Each option must start with the letter followed by closing parenthesis
|
| 498 |
+
- Keep questions practical and engaging with real scenarios
|
| 499 |
+
|
| 500 |
+
MAKE QUESTIONS PRACTICAL AND ENGAGING - Use real scenarios students can relate to!
|
| 501 |
+
"""
|
| 502 |
+
|
| 503 |
+
max_attempts = 3
|
| 504 |
+
all_questions = []
|
| 505 |
+
|
| 506 |
+
for attempt in range(max_attempts):
|
| 507 |
+
try:
|
| 508 |
+
response = self.client.chat.completions.create(
|
| 509 |
+
model="openai/gpt-oss-120b", # More reliable model
|
| 510 |
+
messages=[
|
| 511 |
+
{"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."},
|
| 512 |
+
{"role": "user", "content": base_prompt}
|
| 513 |
+
],
|
| 514 |
+
temperature=0.7, # Balanced creativity
|
| 515 |
+
max_tokens=4000
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
new_questions = self._parse_questions_enhanced(response.choices[0].message.content)
|
| 519 |
+
|
| 520 |
+
# STRICT filtering
|
| 521 |
+
valid_questions = []
|
| 522 |
+
for q in new_questions:
|
| 523 |
+
q_text = q['question_text'].lower()
|
| 524 |
+
|
| 525 |
+
# Check for forbidden keywords
|
| 526 |
+
if any(forbidden in q_text for forbidden in forbidden_keywords):
|
| 527 |
+
continue
|
| 528 |
+
|
| 529 |
+
# Additional checks for visual elements
|
| 530 |
+
if any(word in q_text for word in ['draw', 'sketch', 'plot', 'shown', 'displayed', 'appears']):
|
| 531 |
+
continue
|
| 532 |
+
|
| 533 |
+
# Check uniqueness
|
| 534 |
+
q_hash = self._get_question_hash(q['question_text'] + str(q['options']))
|
| 535 |
+
if q_hash not in seen_hashes:
|
| 536 |
+
q['hash'] = q_hash
|
| 537 |
+
valid_questions.append(q)
|
| 538 |
+
seen_hashes.add(q_hash)
|
| 539 |
+
|
| 540 |
+
all_questions.extend(valid_questions)
|
| 541 |
+
|
| 542 |
+
if len(all_questions) >= num_questions:
|
| 543 |
+
break
|
| 544 |
+
|
| 545 |
+
# Add retry hint with more specific guidance
|
| 546 |
+
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)."
|
| 547 |
+
|
| 548 |
+
except Exception as e:
|
| 549 |
+
st.error(f"Error in attempt {attempt + 1}: {e}")
|
| 550 |
+
continue
|
| 551 |
+
|
| 552 |
+
st.session_state.question_hashes = seen_hashes
|
| 553 |
+
final_questions = all_questions[:num_questions]
|
| 554 |
+
|
| 555 |
+
if len(final_questions) < num_questions:
|
| 556 |
+
st.warning(f"โ ๏ธ Generated {len(final_questions)} unique valid questions (requested {num_questions}). Some were filtered out for containing visual elements.")
|
| 557 |
+
|
| 558 |
+
return final_questions
|
| 559 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
except Exception as e:
|
| 561 |
st.error(f"Failed to generate questions: {e}")
|
| 562 |
return []
|
| 563 |
|
| 564 |
+
def _get_question_hash(self, question_text: str) -> str:
|
| 565 |
+
"""Generate hash for question to track uniqueness"""
|
| 566 |
+
# Remove numbers and normalize text for better uniqueness detection
|
| 567 |
+
normalized = re.sub(r'\d+', 'X', question_text.lower().strip())
|
| 568 |
+
normalized = re.sub(r'[^\w\s]', '', normalized) # Remove punctuation
|
| 569 |
+
return hashlib.md5(normalized.encode()).hexdigest()
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def _parse_questions_enhanced(self, content: str) -> List[Dict]:
|
| 573 |
+
"""Enhanced question parsing with better error handling and validation"""
|
| 574 |
questions = []
|
|
|
|
| 575 |
|
| 576 |
+
# Split by question markers
|
| 577 |
+
question_blocks = re.split(r'\*\*Question \d+:\*\*', content)
|
| 578 |
+
if len(question_blocks) < 2:
|
| 579 |
+
# Try alternative splitting
|
| 580 |
+
question_blocks = re.split(r'Question \d+:', content)
|
| 581 |
+
|
| 582 |
+
for i, block in enumerate(question_blocks[1:], 1):
|
| 583 |
try:
|
| 584 |
lines = [line.strip() for line in block.strip().split('\n') if line.strip()]
|
| 585 |
|
| 586 |
+
if len(lines) < 6: # Need at least question + 4 options + answer
|
| 587 |
continue
|
| 588 |
|
| 589 |
+
question_text = lines[0].strip()
|
| 590 |
+
if not question_text:
|
| 591 |
+
continue
|
| 592 |
+
|
| 593 |
+
# Parse options - FIXED: Create both dictionary and list properly
|
| 594 |
+
options_dict = {}
|
| 595 |
+
options_list = []
|
| 596 |
correct_answer = None
|
| 597 |
explanation = ""
|
| 598 |
hint = ""
|
| 599 |
|
| 600 |
+
current_section = "options"
|
| 601 |
+
|
| 602 |
for line in lines[1:]:
|
| 603 |
+
line = line.strip()
|
| 604 |
+
|
| 605 |
+
# Detect sections
|
| 606 |
+
if line.startswith('**Answer:**') or line.startswith('**Correct Answer:**'):
|
| 607 |
+
current_section = "answer"
|
| 608 |
+
answer_part = line.replace('**Answer:**', '').replace('**Correct Answer:**', '').strip()
|
| 609 |
+
if answer_part:
|
| 610 |
+
# Extract only the letter from the answer (A, B, C, or D)
|
| 611 |
+
answer_match = re.search(r'[ABCD]', answer_part.upper())
|
| 612 |
+
if answer_match:
|
| 613 |
+
correct_answer = answer_match.group()
|
| 614 |
+
continue
|
| 615 |
elif line.startswith('**Explanation:**'):
|
| 616 |
+
current_section = "explanation"
|
| 617 |
+
exp_part = line.replace('**Explanation:**', '').strip()
|
| 618 |
+
if exp_part:
|
| 619 |
+
explanation = exp_part
|
| 620 |
+
continue
|
| 621 |
elif line.startswith('**Hint:**'):
|
| 622 |
+
current_section = "hint"
|
| 623 |
+
hint_part = line.replace('**Hint:**', '').strip()
|
| 624 |
+
if hint_part:
|
| 625 |
+
hint = hint_part
|
| 626 |
+
continue
|
| 627 |
+
|
| 628 |
+
# Process based on current section
|
| 629 |
+
if current_section == "options" and line.startswith(('A)', 'B)', 'C)', 'D)')):
|
| 630 |
+
option_key = line[0] # A, B, C, or D
|
| 631 |
+
option_text = line[2:].strip() # Remove "A) "
|
| 632 |
+
options_dict[option_key] = option_text
|
| 633 |
+
options_list.append(line) # Keep full format "A) text"
|
| 634 |
+
elif current_section == "answer":
|
| 635 |
+
# Try to extract single letter answer
|
| 636 |
+
if line.strip() in ['A', 'B', 'C', 'D']:
|
| 637 |
+
correct_answer = line.strip()
|
| 638 |
+
elif not correct_answer:
|
| 639 |
+
# Fallback: extract first letter found
|
| 640 |
+
answer_match = re.search(r'[ABCD]', line.upper())
|
| 641 |
+
if answer_match:
|
| 642 |
+
correct_answer = answer_match.group()
|
| 643 |
+
elif current_section == "explanation" and line:
|
| 644 |
+
if explanation:
|
| 645 |
+
explanation += " " + line
|
| 646 |
+
else:
|
| 647 |
+
explanation = line
|
| 648 |
+
elif current_section == "hint" and line:
|
| 649 |
+
if hint:
|
| 650 |
+
hint += " " + line
|
| 651 |
+
else:
|
| 652 |
+
hint = line
|
| 653 |
|
| 654 |
+
# Validate question - ensure we have exactly 4 options and a valid answer
|
| 655 |
+
if len(options_dict) == 4 and correct_answer and correct_answer in options_dict:
|
| 656 |
questions.append({
|
| 657 |
'question_number': i,
|
| 658 |
'question_text': question_text,
|
| 659 |
+
'options': options_list, # List format for compatibility: ['A) text', 'B) text', ...]
|
| 660 |
+
'options_dict': options_dict, # Dictionary format: {'A': 'text', 'B': 'text', ...}
|
| 661 |
'correct_answer': correct_answer,
|
| 662 |
+
'explanation': explanation or "Work through the problem step by step.",
|
| 663 |
+
'hint': hint or "Read the question carefully and identify what you need to find.",
|
| 664 |
'user_answer': None,
|
| 665 |
'time_taken': 0,
|
| 666 |
'difficulty': self._estimate_difficulty(question_text)
|
| 667 |
})
|
| 668 |
+
else:
|
| 669 |
+
st.warning(f"Question {i} validation failed: options={len(options_dict)}, answer='{correct_answer}', keys={list(options_dict.keys())}")
|
| 670 |
|
| 671 |
except Exception as e:
|
| 672 |
st.warning(f"Error parsing question {i}: {e}")
|
|
|
|
| 676 |
|
| 677 |
def _estimate_difficulty(self, question_text: str) -> str:
|
| 678 |
"""Estimate question difficulty based on content"""
|
| 679 |
+
hard_keywords = ['system', 'equation', 'quadratic', 'prove', 'theorem', 'complex', 'advanced']
|
| 680 |
+
easy_keywords = ['add', 'subtract', 'simple', 'basic', 'find the', 'what is', 'how many']
|
| 681 |
|
| 682 |
text_lower = question_text.lower()
|
| 683 |
|
|
|
|
| 714 |
'badges': [],
|
| 715 |
'streak': 0
|
| 716 |
}
|
| 717 |
+
if 'question_hashes' not in st.session_state:
|
| 718 |
+
st.session_state.question_hashes = set()
|
| 719 |
|
| 720 |
@st.cache_resource
|
| 721 |
def get_exam_system():
|
|
|
|
| 728 |
<div class="hero-title">๐งฎ MathGenius Academy</div>
|
| 729 |
<div class="hero-subtitle">Master Mathematics Through Interactive Olympiad Training</div>
|
| 730 |
<div style="margin-top: 1rem;">
|
| 731 |
+
<span style="background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 25px; margin: 0 0.5rem;">๐ฏ Practical Problems</span>
|
| 732 |
<span style="background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 25px; margin: 0 0.5rem;">๐ Progress Tracking</span>
|
| 733 |
<span style="background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 25px; margin: 0 0.5rem;">๐ Achievement System</span>
|
| 734 |
</div>
|
| 735 |
</div>
|
| 736 |
+
""", unsafe_allow_html = True)
|
| 737 |
+
|
| 738 |
+
def render_timer_display():
|
| 739 |
+
"""Render enhanced timer display with better positioning"""
|
| 740 |
+
if st.session_state.exam_mode == 'timed' and st.session_state.start_time:
|
| 741 |
+
total_time = len(st.session_state.questions) * 60 # 1 minute per question
|
| 742 |
+
elapsed = time.time() - st.session_state.start_time
|
| 743 |
+
remaining = max(0, total_time - elapsed)
|
| 744 |
+
|
| 745 |
+
mins, secs = divmod(int(remaining), 60)
|
| 746 |
+
|
| 747 |
+
# Add warning class if time is running low
|
| 748 |
+
warning_class = "timer-warning" if remaining <= 60 else ""
|
| 749 |
+
|
| 750 |
+
timer_html = f"""
|
| 751 |
+
<div class="timer-display {warning_class}">
|
| 752 |
+
<div style="display: flex; align-items: center; justify-content: center; gap: 1rem;">
|
| 753 |
+
<div style="font-size: 3rem;">โฐ</div>
|
| 754 |
+
<div>
|
| 755 |
+
<div style="font-size: 2.5rem; font-weight: 800;">{mins:02d}:{secs:02d}</div>
|
| 756 |
+
<div style="font-size: 1rem; opacity: 0.9;">Time Remaining</div>
|
| 757 |
+
</div>
|
| 758 |
+
</div>
|
| 759 |
+
</div>
|
| 760 |
+
"""
|
| 761 |
+
|
| 762 |
+
st.markdown(timer_html, unsafe_allow_html=True)
|
| 763 |
+
|
| 764 |
+
# Auto-submit when time is up
|
| 765 |
+
if remaining <= 0:
|
| 766 |
+
st.error("โฐ Time's up! Auto-submitting your exam...")
|
| 767 |
+
st.session_state.exam_completed = True
|
| 768 |
+
time.sleep(1)
|
| 769 |
+
st.rerun()
|
| 770 |
+
|
| 771 |
+
# Show warning when time is low
|
| 772 |
+
elif remaining <= 60:
|
| 773 |
+
st.warning(f"โ ๏ธ Only {int(remaining)} seconds left!")
|
| 774 |
|
| 775 |
def render_dashboard():
|
| 776 |
"""Render user dashboard with statistics"""
|
|
|
|
| 829 |
paper_bgcolor='rgba(0,0,0,0)',
|
| 830 |
)
|
| 831 |
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 832 |
|
| 833 |
def render_question_card(question, question_index, total_questions):
|
| 834 |
+
"""Render an individual question with enhanced UI - FIXED"""
|
| 835 |
progress = ((question_index + 1) / total_questions) * 100
|
| 836 |
|
| 837 |
st.markdown(f"""
|
|
|
|
| 856 |
</div>
|
| 857 |
""", unsafe_allow_html=True)
|
| 858 |
|
| 859 |
+
# FIXED: Handle both list and dictionary options formats
|
| 860 |
+
if isinstance(question['options'], list):
|
| 861 |
+
# List format: ['A) text', 'B) text', ...]
|
| 862 |
+
option_keys = []
|
| 863 |
+
option_labels = []
|
| 864 |
+
for opt in question['options']:
|
| 865 |
+
if ')' in opt:
|
| 866 |
+
key, label = opt.split(')', 1)
|
| 867 |
+
option_keys.append(key.strip())
|
| 868 |
+
option_labels.append(label.strip())
|
| 869 |
+
|
| 870 |
+
# Create format function for radio buttons
|
| 871 |
+
def format_option(key):
|
| 872 |
+
try:
|
| 873 |
+
idx = option_keys.index(key)
|
| 874 |
+
return f"{key}) {option_labels[idx]}"
|
| 875 |
+
except (ValueError, IndexError):
|
| 876 |
+
return f"{key}) Option not found"
|
| 877 |
+
|
| 878 |
+
selected = st.radio(
|
| 879 |
+
"Choose your answer:",
|
| 880 |
+
options=option_keys,
|
| 881 |
+
format_func=format_option,
|
| 882 |
+
key=f"q_{question_index}",
|
| 883 |
+
index=None
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
elif isinstance(question['options'], dict):
|
| 887 |
+
# Dictionary format: {'A': 'text', 'B': 'text', ...}
|
| 888 |
+
option_keys = list(question['options'].keys())
|
| 889 |
+
|
| 890 |
+
def format_option_dict(key):
|
| 891 |
+
return f"{key}) {question['options'].get(key, 'Option not found')}"
|
| 892 |
+
|
| 893 |
+
selected = st.radio(
|
| 894 |
+
"Choose your answer:",
|
| 895 |
+
options=option_keys,
|
| 896 |
+
format_func=format_option_dict,
|
| 897 |
+
key=f"q_{question_index}",
|
| 898 |
+
index=None
|
| 899 |
+
)
|
| 900 |
|
| 901 |
+
else:
|
| 902 |
+
st.error("Invalid options format in question data")
|
| 903 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
|
| 905 |
# Hint system
|
| 906 |
if st.button(f"๐ก Need a hint?", key=f"hint_{question_index}"):
|
|
|
|
| 912 |
return selected
|
| 913 |
|
| 914 |
def render_exam_interface():
|
| 915 |
+
"""Render the main exam interface with improved timer - FIXED"""
|
| 916 |
questions = st.session_state.questions
|
| 917 |
total_questions = len(questions)
|
| 918 |
|
| 919 |
+
# Display timer at the top for timed mode
|
| 920 |
+
render_timer_display()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 921 |
|
| 922 |
# Question navigation
|
| 923 |
if total_questions > 1:
|
|
|
|
| 932 |
# Current question
|
| 933 |
current_q = st.session_state.current_question
|
| 934 |
if current_q < total_questions:
|
| 935 |
+
# FIXED: Call render_question_card as a standalone function
|
| 936 |
selected = render_question_card(questions[current_q], current_q, total_questions)
|
| 937 |
questions[current_q]['user_answer'] = selected
|
| 938 |
|
|
|
|
| 980 |
return badges
|
| 981 |
|
| 982 |
def render_results_section():
|
| 983 |
+
"""Render enhanced results with animations and insights"""
|
| 984 |
st.markdown("---")
|
| 985 |
st.markdown("### ๐ Exam Results")
|
| 986 |
|
|
|
|
| 1073 |
if not is_correct and question.get('hint'):
|
| 1074 |
st.info(f"๐ก **Hint for next time:** {question['hint']}")
|
| 1075 |
|
| 1076 |
+
# Performance insights with bullet points
|
| 1077 |
exam_system = get_exam_system()
|
| 1078 |
if exam_system.client:
|
| 1079 |
st.markdown("#### ๐ง AI Performance Analysis")
|
|
|
|
| 1092 |
</div>
|
| 1093 |
""", unsafe_allow_html=True)
|
| 1094 |
|
| 1095 |
+
# Show completion message
|
| 1096 |
st.markdown("#### ๐ Exam Complete!")
|
| 1097 |
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.")
|
| 1098 |
|
| 1099 |
def generate_enhanced_summary(grade, questions, score, total, exam_system):
|
| 1100 |
+
"""Generate enhanced AI-powered performance summary in bullet points"""
|
| 1101 |
try:
|
| 1102 |
# Analyze performance patterns
|
| 1103 |
correct_questions = [q for q in questions if q['user_answer'] == q['correct_answer']]
|
|
|
|
| 1123 |
SAMPLE INCORRECT QUESTIONS:
|
| 1124 |
{chr(10).join([f"- {q['question_text'][:100]}..." for q in incorrect_questions[:2]])}
|
| 1125 |
|
| 1126 |
+
Please provide a summary in BULLET POINTS with emojis using this exact format:
|
| 1127 |
+
|
| 1128 |
+
๐ฏ **Strengths:**
|
| 1129 |
+
โข [strength 1 with emoji]
|
| 1130 |
+
โข [strength 2 with emoji]
|
| 1131 |
+
|
| 1132 |
+
๐ **Areas to Improve:**
|
| 1133 |
+
โข [area to improve 1 with emoji]
|
| 1134 |
+
โข [area to improve 2 with emoji]
|
| 1135 |
+
|
| 1136 |
+
๐ก **Quick Tips:**
|
| 1137 |
+
โข [tip 1 with emoji]
|
| 1138 |
+
โข [tip 2 with emoji]
|
| 1139 |
+
|
| 1140 |
+
๐ **Motivation:**
|
| 1141 |
+
โข [encouraging message with emoji]
|
| 1142 |
+
|
| 1143 |
+
Use Grade {grade} appropriate language and keep each bullet point short and encouraging. Include relevant emojis for each point."""
|
| 1144 |
|
| 1145 |
response = exam_system.client.chat.completions.create(
|
| 1146 |
+
model="openai/gpt-oss-120b",
|
| 1147 |
messages=[
|
| 1148 |
+
{"role": "system", "content": "You are an expert Math Olympiad mentor providing detailed, encouraging feedback in bullet points with emojis."},
|
| 1149 |
{"role": "user", "content": prompt}
|
| 1150 |
],
|
| 1151 |
+
temperature=0,
|
| 1152 |
max_tokens=1000
|
| 1153 |
)
|
| 1154 |
|
|
|
|
| 1156 |
|
| 1157 |
except Exception as e:
|
| 1158 |
return f"""
|
| 1159 |
+
๐ฏ **Strengths:**
|
| 1160 |
+
โข โจ You completed {score} out of {total} questions correctly ({percentage:.1f}%)
|
| 1161 |
+
โข ๐ช You showed good problem-solving skills
|
| 1162 |
+
|
| 1163 |
+
๐ **Areas to Improve:**
|
| 1164 |
+
โข ๐ Review the questions you missed for better understanding
|
| 1165 |
+
โข โฐ Work on time management if needed
|
| 1166 |
+
|
| 1167 |
+
๐ก **Quick Tips:**
|
| 1168 |
+
โข ๐ Read each question carefully before answering
|
| 1169 |
+
โข ๐ญ Use hints when you're unsure
|
| 1170 |
+
|
| 1171 |
+
๐ **Motivation:**
|
| 1172 |
+
โข ๐ Great effort! Keep practicing and you'll improve even more!
|
| 1173 |
"""
|
| 1174 |
|
| 1175 |
def reset_exam():
|
|
|
|
| 1215 |
["practice", "timed", "challenge"],
|
| 1216 |
format_func=lambda x: {
|
| 1217 |
"practice": "๐ฏ Practice Mode",
|
| 1218 |
+
"timed": "โฐ Timed Mode (1 min/question)",
|
| 1219 |
"challenge": "๐ฅ Challenge Mode"
|
| 1220 |
}[x],
|
| 1221 |
+
help="Practice: No time limit | Timed: 1 minute per question | Challenge: Extra hard questions"
|
| 1222 |
)
|
| 1223 |
st.session_state.exam_mode = exam_mode
|
| 1224 |
|
|
|
|
| 1228 |
else:
|
| 1229 |
num_questions = st.slider("๐ Questions:", 3, 15, 5, help="Choose number of questions")
|
| 1230 |
|
| 1231 |
+
# Show timer info for timed mode
|
| 1232 |
+
if exam_mode == "timed":
|
| 1233 |
+
st.info(f"โฐ Total time: {num_questions} minutes ({num_questions} questions ร 1 min each)")
|
| 1234 |
+
|
| 1235 |
# Difficulty level
|
| 1236 |
difficulty = st.select_slider(
|
| 1237 |
"โก Difficulty:",
|
|
|
|
| 1249 |
if not exam_system.client:
|
| 1250 |
st.error("๐ซ API not available!")
|
| 1251 |
else:
|
| 1252 |
+
with st.spinner(f"๐ญ Creating your {exam_mode} exam with unique questions..."):
|
| 1253 |
# Add loading animation
|
| 1254 |
progress_bar = st.progress(0)
|
| 1255 |
for i in range(100):
|
|
|
|
| 1260 |
questions = exam_system.generate_questions(grade, num_questions, challenge_difficulty)
|
| 1261 |
|
| 1262 |
if questions:
|
| 1263 |
+
st.session_state.questions = questions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1264 |
st.session_state.exam_completed = False
|
| 1265 |
st.session_state.grade_selected = grade
|
| 1266 |
st.session_state.current_question = 0
|
| 1267 |
st.session_state.start_time = time.time() if exam_mode == "timed" else None
|
| 1268 |
|
| 1269 |
+
st.success(f"โจ Generated {len(st.session_state.questions)} unique questions!")
|
| 1270 |
st.balloons()
|
| 1271 |
else:
|
| 1272 |
st.error("โ Failed to generate exam. Try again!")
|
|
|
|
| 1326 |
st.markdown("""
|
| 1327 |
<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;">
|
| 1328 |
<div style="font-size: 3rem; margin-bottom: 1rem;">๐ฏ</div>
|
| 1329 |
+
<h4 style="margin: 0.5rem 0; color: white;">Unique Questions</h4>
|
| 1330 |
+
<p style="margin: 0; color: rgba(255,255,255,0.9);">Always new, never repeated</p>
|
| 1331 |
</div>
|
| 1332 |
""", unsafe_allow_html=True)
|
| 1333 |
|
| 1334 |
with col2:
|
| 1335 |
st.markdown("""
|
| 1336 |
<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;">
|
| 1337 |
+
<div style="font-size: 3rem; margin-bottom: 1rem;">๐</div>
|
| 1338 |
+
<h4 style="margin: 0.5rem 0; color: white;">Text-Based Only</h4>
|
| 1339 |
+
<p style="margin: 0; color: rgba(255,255,255,0.9);">Pure math, no visual puzzles</p>
|
| 1340 |
</div>
|
| 1341 |
""", unsafe_allow_html=True)
|
| 1342 |
|
| 1343 |
with col3:
|
| 1344 |
st.markdown("""
|
| 1345 |
<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;">
|
| 1346 |
+
<div style="font-size: 3rem; margin-bottom: 1rem;">โฐ</div>
|
| 1347 |
+
<h4 style="margin: 0.5rem 0; color: white;">Smart Timer</h4>
|
| 1348 |
+
<p style="margin: 0; color: rgba(255,255,255,0.9);">1 minute per question</p>
|
| 1349 |
</div>
|
| 1350 |
""", unsafe_allow_html=True)
|
| 1351 |
|
|
|
|
| 1361 |
1. **๐ฏ Choose Your Grade**: Select your current grade level from the sidebar
|
| 1362 |
2. **๐ Pick Your Mode**:
|
| 1363 |
- ๐ข **Practice Mode**: No time pressure, perfect for learning
|
| 1364 |
+
- โฐ **Timed Mode**: 1 minute per question challenge
|
| 1365 |
- ๐ฅ **Challenge Mode**: Extra difficult questions for advanced learners
|
| 1366 |
3. **โก Set Difficulty**: Easy, Medium, or Hard - pick what feels right
|
| 1367 |
4. **๐ Choose Questions**: 3-20 questions depending on your time
|
|
|
|
| 1409 |
else:
|
| 1410 |
st.markdown("### ๐งฎ Math Olympiad Challenge")
|
| 1411 |
|
| 1412 |
+
# Show exam info
|
| 1413 |
+
col1, col2, col3 = st.columns(3)
|
| 1414 |
+
with col1:
|
| 1415 |
+
st.info(f"๐ Grade {st.session_state.grade_selected}")
|
| 1416 |
+
with col2:
|
| 1417 |
+
if st.session_state.exam_mode == "timed":
|
| 1418 |
+
total_time = len(st.session_state.questions)
|
| 1419 |
+
st.info(f"โฑ๏ธ {st.session_state.exam_mode.title()} Mode ({total_time} min)")
|
| 1420 |
+
else:
|
| 1421 |
st.info(f"โฑ๏ธ {st.session_state.exam_mode.title()} Mode")
|
| 1422 |
+
with col3:
|
| 1423 |
+
st.info(f"๐ {len(st.session_state.questions)} Questions")
|
| 1424 |
|
| 1425 |
render_exam_interface()
|
| 1426 |
|
|
|
|
| 1504 |
<div class="custom-card">
|
| 1505 |
<h4 style="color: #667eea;">๐ค AI-Powered Learning</h4>
|
| 1506 |
<ul style="color: #444;">
|
| 1507 |
+
<li><strong>Unique Questions:</strong> Never see the same question twice</li>
|
| 1508 |
<li><strong>Smart Difficulty:</strong> Automatic adjustment based on performance</li>
|
| 1509 |
+
<li><strong>Text-Only Focus:</strong> Pure mathematical reasoning, no visual puzzles</li>
|
| 1510 |
</ul>
|
| 1511 |
<h4 style="color: #667eea;">๐ฎ Multiple Game Modes</h4>
|
| 1512 |
<ul style="color: #444;">
|
| 1513 |
<li><strong>Practice Mode:</strong> Learn at your own pace</li>
|
| 1514 |
+
<li><strong>Timed Mode:</strong> 1 minute per question challenge</li>
|
| 1515 |
<li><strong>Challenge Mode:</strong> Push your limits with hard problems</li>
|
| 1516 |
</ul>
|
| 1517 |
</div>
|
|
|
|
| 1526 |
<h4 style="color: #667eea;">๐ Progress & Analytics</h4>
|
| 1527 |
<ul style="color: #444;">
|
| 1528 |
<li><strong>Performance Tracking:</strong> Monitor improvement over time</li>
|
| 1529 |
+
<li><strong>Detailed Insights:</strong> Bullet-point feedback with emojis</li>
|
| 1530 |
<li><strong>Visual Charts:</strong> Interactive graphs and analysis</li>
|
| 1531 |
</ul>
|
| 1532 |
<h4 style="color: #667eea;">๐ Achievement System</h4>
|
|
|
|
| 1585 |
<ul style="color: #444;">
|
| 1586 |
<li>Basic arithmetic operations</li>
|
| 1587 |
<li>Simple word problems</li>
|
| 1588 |
+
<li>Number patterns and sequences</li>
|
| 1589 |
+
<li>Basic geometry concepts</li>
|
| 1590 |
<li>Counting and number sense</li>
|
| 1591 |
</ul>
|
| 1592 |
</div>
|
|
|
|
| 1623 |
# FAQ Section
|
| 1624 |
with st.expander("โ Frequently Asked Questions"):
|
| 1625 |
st.markdown("""
|
| 1626 |
+
**Q: How are questions made unique?**
|
| 1627 |
|
| 1628 |
+
A: We use advanced AI with hash tracking to ensure you never see the same question twice, even across different sessions.
|
| 1629 |
|
| 1630 |
+
**Q: Why text-based questions only?**
|
| 1631 |
|
| 1632 |
+
A: We focus purely on mathematical reasoning and calculations, avoiding visual puzzles that don't translate well to text interfaces.
|
| 1633 |
|
| 1634 |
+
**Q: How does the timer work in timed mode?**
|
| 1635 |
|
| 1636 |
+
A: You get exactly 1 minute per question. For 5 questions, you get 5 minutes total. The timer is prominently displayed above the questions.
|
| 1637 |
|
| 1638 |
**Q: What if I get stuck on a question?**
|
| 1639 |
|
| 1640 |
A: Use the hint feature! We provide helpful hints to guide your thinking without giving away the answer.
|
| 1641 |
|
| 1642 |
+
**Q: How is my progress tracked?**
|
| 1643 |
|
| 1644 |
+
A: We track your accuracy, response times, difficulty preferences, and improvement trends with detailed analytics.
|
| 1645 |
|
| 1646 |
**Q: How do I earn badges?**
|
| 1647 |
|
|
|
|
| 1654 |
<div style="text-align: center; padding: 2rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20px; color: white;">
|
| 1655 |
<h3 style="color: white; margin-bottom: 1rem;">Ready to become a Math Genius? ๐งฎ</h3>
|
| 1656 |
<p style="color: rgba(255,255,255,0.9); margin: 0;">
|
| 1657 |
+
Join thousands of students improving their mathematical skills with unique, challenging questions every day!
|
| 1658 |
</p>
|
| 1659 |
</div>
|
| 1660 |
""", unsafe_allow_html=True)
|
|
|
|
| 1663 |
st.markdown("**Made with โค๏ธ for young mathematicians everywhere!**")
|
| 1664 |
|
| 1665 |
if __name__ == "__main__":
|
| 1666 |
+
main()
|
| 1667 |
+
|