import streamlit as st
import pandas as pd
import json
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime, timedelta
import google.generativeai as genai
import os
import re
from pathlib import Path
import math
import time
# ============================================
# π CONFIGURATION FOR HUGGING FACE DEPLOYMENT
# ============================================
# Page configuration
st.set_page_config(
page_title="AI Study Planner Pro",
page_icon="π",
layout="wide",
initial_sidebar_state="collapsed"
)
# Get API Key from Hugging Face Secrets
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
# ============================================
# π¨ CUSTOM CSS WITH IMPROVED STYLING
# ============================================
st.markdown("""
""", unsafe_allow_html=True)
# ============================================
# π§ SESSION STATE INITIALIZATION - FIXED
# ============================================
def init_session_state():
"""Initialize all session state variables"""
session_vars = {
'study_plan': None,
'progress': {},
'subject': "",
'api_key': GEMINI_API_KEY,
'raw_response': "",
'show_debug': False,
'study_days': ["Mon", "Tue", "Wed", "Thu", "Fri"],
'plan_loaded': False,
'current_week': 1,
'task_states': {},
'weekly_streak': 0,
'longest_weekly_streak': 0,
'task_completion_dates': {},
'daily_tips': {},
'test_results': {},
'current_test': None,
'test_start_time': None,
'test_questions': [],
'user_test_answers': {},
'test_completed': False,
'timer_seconds': 1200,
'flexible_completion': True, # ADDED THIS LINE
'daily_progress': [] # ADDED THIS LINE
}
for key, value in session_vars.items():
if key not in st.session_state:
st.session_state[key] = value
init_session_state()
# ============================================
# π οΈ UPDATED UTILITY FUNCTIONS WITH BETTER JSON PARSING
# ============================================
def extract_and_fix_json(text, subject="General"):
"""
Extract JSON from text and fix common issues with improved handling
"""
if not text or not isinstance(text, str):
if st.session_state.show_debug:
st.warning("β οΈ No text to parse in extract_and_fix_json")
return None
# Store raw response for debug
st.session_state.raw_response = text
# Clean the text - more aggressive cleaning
text = text.strip()
# Check if response indicates safety blocking
if "safety" in text.lower() or "blocked" in text.lower() or "not allowed" in text.lower():
if st.session_state.show_debug:
st.error("β Response indicates safety blocking")
return None
# Remove ALL markdown code blocks and any surrounding text
text = re.sub(r'.*?```(?:json)?\s*', '', text, flags=re.IGNORECASE | re.DOTALL)
text = re.sub(r'```.*', '', text, flags=re.IGNORECASE | re.DOTALL)
# Remove any non-JSON text before first {
first_brace = text.find('{')
if first_brace > 0:
text = text[first_brace:]
# Remove any non-JSON text after last }
last_brace = text.rfind('}')
if last_brace != -1:
text = text[:last_brace+1]
# Show cleaned text in debug
if st.session_state.show_debug:
with st.expander("π§Ή Cleaned Text", expanded=False):
st.code(text[:500] + "..." if len(text) > 500 else text)
# Find JSON boundaries
start_idx = text.find('{')
end_idx = text.rfind('}')
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
if st.session_state.show_debug:
st.error(f"β No JSON found. Start: {start_idx}, End: {end_idx}")
st.info(f"Text preview: {text[:200]}...")
return None
json_str = text[start_idx:end_idx+1]
# Show JSON string in debug
if st.session_state.show_debug:
with st.expander("π JSON String Being Parsed", expanded=False):
st.code(json_str[:800] + "..." if len(json_str) > 800 else json_str)
try:
# First try: Direct parse
parsed_json = json.loads(json_str)
if st.session_state.show_debug:
st.success("β
JSON parsed successfully on first try")
return parsed_json
except json.JSONDecodeError as e:
if st.session_state.show_debug:
st.warning(f"β οΈ First parse failed: {str(e)}")
st.info("Trying to fix JSON...")
# Second try: Apply advanced fixes
json_str = fix_json_string_advanced(json_str)
try:
parsed_json = json.loads(json_str)
if st.session_state.show_debug:
st.success("β
JSON parsed successfully after advanced fixes")
return parsed_json
except json.JSONDecodeError as e2:
if st.session_state.show_debug:
st.error(f"β Second parse failed: {str(e2)}")
# Try one more time with even more aggressive fixing
try:
json_str = fix_json_string_aggressive(json_str)
parsed_json = json.loads(json_str)
if st.session_state.show_debug:
st.success("β
JSON parsed after aggressive fixes")
return parsed_json
except json.JSONDecodeError as e3:
st.error(f"β Final parse failed: {str(e3)}")
return None
def fix_json_string_advanced(json_str):
"""
Fix common JSON issues in AI responses based on actual patterns observed
"""
if not json_str:
return "{}"
# 1. Fix unquoted keys (e.g., week: 1 -> "week": 1)
json_str = re.sub(r'([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:', r'\1"\2":', json_str)
# 2. Fix trailing commas before } or ]
json_str = re.sub(r',\s*}', '}', json_str)
json_str = re.sub(r',\s*]', ']', json_str)
# 3. Fix missing commas between objects in arrays
json_str = re.sub(r'}\s*{', '},{', json_str)
# 4. Fix missing commas after values
json_str = re.sub(r'(? 1:
# Escape quotes that are inside strings
for i in range(1, len(parts)-1, 2):
if parts[i] and not parts[i].endswith('\\'):
# This is a string value that might have unescaped quotes
pass
fixed_lines.append(line)
json_str = '\n'.join(fixed_lines)
# 6. Fix markdown bold/italic in strings (remove ** and *)
json_str = re.sub(r'\*\*(.*?)\*\*', r'\1', json_str)
json_str = re.sub(r'\*(.*?)\*', r'\1', json_str)
# 7. Remove any control characters except newlines
json_str = ''.join(char for char in json_str if ord(char) >= 32 or char in '\n\r\t')
# 8. Fix common pattern: "column 6" -> this usually means missing comma in array
# Look for patterns like: "something": [ ... "item1" "item2" ... ]
json_str = re.sub(r'("[^"]*")(\s+"[^"]*")', r'\1,\2', json_str)
return json_str
def fix_json_string_aggressive(json_str):
"""
Even more aggressive JSON fixing for problematic AI responses
"""
if not json_str:
return "{}"
# Try to find the main JSON structure
start = json_str.find('{')
end = json_str.rfind('}')
if start >= 0 and end > start:
json_str = json_str[start:end+1]
# Replace single quotes with double quotes (carefully)
# Only replace single quotes that are not escaped and appear to be string delimiters
in_string = False
result = []
i = 0
while i < len(json_str):
if json_str[i] == '"' and (i == 0 or json_str[i-1] != '\\'):
in_string = not in_string
result.append('"')
elif json_str[i] == "'" and not in_string:
# Check if this looks like a string delimiter
if i > 0 and json_str[i-1] in [' ', ':', ',', '[', '{']:
result.append('"')
else:
result.append("'")
else:
result.append(json_str[i])
i += 1
json_str = ''.join(result)
# Fix missing commas in arrays (aggressive)
# Pattern: value whitespace value where value is string, number, or boolean
json_str = re.sub(r'("[^"]*"|\d+\.?\d*|true|false|null)(\s+)(?="|\d|t|f|n|\[|\{)', r'\1,\2', json_str, flags=re.IGNORECASE)
return json_str
def extract_test_json(text):
"""
Extract test JSON from AI response
"""
if not text or not isinstance(text, str):
return None
text = text.strip()
# Remove markdown code blocks
text = re.sub(r'```json\s*', '', text, flags=re.IGNORECASE)
text = re.sub(r'```\s*', '', text, flags=re.IGNORECASE)
# Find JSON
start_idx = text.find('{')
end_idx = text.rfind('}')
if start_idx == -1 or end_idx == -1:
return None
json_str = text[start_idx:end_idx+1]
try:
return json.loads(json_str)
except json.JSONDecodeError:
# Apply fixes
json_str = fix_json_string_advanced(json_str)
try:
return json.loads(json_str)
except:
return None
def calculate_learning_quality(days_available, hours_per_day, study_days_count):
"""
Calculate learning quality metrics
"""
total_hours = days_available * hours_per_day
required_hours = 80 # Default for medium complexity
coverage = min(100, (total_hours / required_hours) * 100)
quality_score = 0
if 1 <= hours_per_day <= 3:
quality_score += 30
elif hours_per_day > 3:
quality_score += 20
else:
quality_score += 10
if 3 <= study_days_count <= 5:
quality_score += 30
elif study_days_count > 5:
quality_score += 20
else:
quality_score += 10
if coverage >= 80:
quality_score += 40
elif coverage >= 50:
quality_score += 20
else:
quality_score += 10
return {
"quality_score": min(100, quality_score),
"coverage_percentage": coverage,
"total_hours": total_hours,
"required_hours": required_hours,
"weekly_hours": hours_per_day * study_days_count,
"is_optimal": quality_score >= 60
}
def create_generic_plan(subject, days_available):
"""
Create generic plan if AI fails
"""
weeks = max(1, days_available // 7)
daily_hours = 2
study_days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
plan = {
"subject": subject,
"total_weeks": weeks,
"daily_hours": daily_hours,
"study_days": study_days,
"topics_allocation": {
"Fundamentals": 35,
"Core Concepts": 30,
"Practice": 25,
"Advanced": 10
},
"weekly_schedule": [],
"study_tips": [
"Set specific daily goals",
"Review previous material",
"Take regular breaks",
"Practice what you learn"
],
"success_metrics": [
f"Complete {weeks} weeks of study",
"Demonstrate understanding through practice",
"Apply knowledge in real scenarios"
]
}
for week_num in range(1, min(weeks, 12) + 1):
daily_tasks = []
for day_name in study_days:
task1 = f"Study {subject} topics"
task2 = f"Practice {subject} exercises"
daily_tasks.append([task1, task2])
week_plan = {
"week": week_num,
"focus": f"Week {week_num}: {subject} Fundamentals",
"objectives": ["Understand core concepts", "Complete practice exercises"],
"daily_tasks": daily_tasks,
"day_focus": [f"Learn basic concepts", "Practice exercises", "Review", "Apply knowledge", "Project work"],
"week_summary": f"This week focuses on building fundamental knowledge of {subject}.",
"resources": ["Online courses", "Documentation", "Practice platforms"],
"milestone": f"Complete Week {week_num} objectives",
"completed": False,
"tasks_completed": 0,
"total_tasks": len(study_days) * 2
}
plan["weekly_schedule"].append(week_plan)
return plan
# ============================================
# π€ UPDATED AI STUDY PLAN GENERATION WITH BETTER PROMPT
# ============================================
def generate_study_plan(api_key, **kwargs):
"""
Generate study plan using Gemini with safety handling
"""
if not api_key:
st.error("β No API key provided")
return create_generic_plan(kwargs.get('subject', 'General'),
kwargs.get('days_available', 60))
# Extract parameters
subject = kwargs.get('subject', 'General Learning')
days_available = kwargs.get('days_available', 60)
hours_per_day = kwargs.get('hours_per_day', 2)
current_level = kwargs.get('current_level', 'Beginner')
intensity = kwargs.get('intensity', 'Moderate')
study_days = kwargs.get('study_days', ["Mon", "Tue", "Wed", "Thu", "Fri"])
weeks = max(1, days_available // 7)
# IMPROVED PROMPT with better JSON formatting instructions
prompt = f"""Create a detailed {weeks}-week study plan for learning: {subject}
Study schedule: {hours_per_day} hours/day, {len(study_days)} days/week ({', '.join(study_days)})
Current level: {current_level}
Intensity: {intensity}
IMPORTANT: Return ONLY valid JSON with this exact structure:
{{
"subject": "{subject}",
"total_weeks": {weeks},
"daily_hours": {hours_per_day},
"study_days": {json.dumps(study_days)},
"topics_allocation": {{
"Topic 1": percentage_number,
"Topic 2": percentage_number
}},
"weekly_schedule": [
{{
"week": 1,
"focus": "Week focus title",
"objectives": ["Objective 1", "Objective 2"],
"daily_tasks": [
["Task 1 for Monday", "Task 2 for Monday"],
["Task 1 for Tuesday", "Task 2 for Tuesday"],
["Task 1 for Wednesday", "Task 2 for Wednesday"],
["Task 1 for Thursday", "Task 2 for Thursday"],
["Task 1 for Friday", "Task 2 for Friday"]
],
"day_focus": [
"Monday: Introduction",
"Tuesday: Practice",
"Wednesday: Deep Dive",
"Thursday: Review",
"Friday: Project"
],
"week_summary": "Brief summary of week's learning objectives",
"resources": ["Resource 1", "Resource 2"],
"milestone": "What to achieve by week's end"
}}
],
"study_tips": ["Tip 1", "Tip 2"],
"success_metrics": ["Metric 1", "Metric 2"]
}}
CRITICAL RULES FOR VALID JSON:
1. Use double quotes ONLY for ALL strings and keys
2. Separate array items with commas
3. End object properties with commas (except last one before closing brace)
4. topics_allocation values must be NUMBERS (not strings with %)
5. Include EXACTLY {weeks} weeks in weekly_schedule array
6. Each week must have daily_tasks array with {len(study_days)} sub-arrays
7. NO trailing commas after last array element or object property
8. NO markdown formatting (no **bold**, no *italic*)
9. Escape quotes inside strings with backslash (\")
10. Ensure all brackets and braces are properly closed
Make the plan practical, educational, and appropriate for all ages."""
try:
# Configure API with safety settings
genai.configure(api_key=api_key)
model = genai.GenerativeModel('gemini-2.5-flash')
# Show prompt in debug mode
if st.session_state.show_debug:
with st.expander("π Prompt Sent to AI", expanded=False):
st.code(prompt[:800] + "..." if len(prompt) > 800 else prompt)
# Generate response with safety settings
response = model.generate_content(
prompt,
generation_config=genai.GenerationConfig(
max_output_tokens=4000,
temperature=0.7,
top_p=0.95,
top_k=40
),
safety_settings=[
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}
]
)
# SAFE way to get response text
if response and hasattr(response, 'text'):
raw_text = response.text
else:
raw_text = ""
st.session_state.raw_response = raw_text
# Show raw response in debug mode
if st.session_state.show_debug:
with st.expander("π Raw AI Response", expanded=False):
if raw_text:
st.text_area("AI Response", raw_text, height=200, key="ai_response_raw_gen")
else:
st.warning("No response text received")
# Extract JSON
plan = extract_and_fix_json(raw_text, subject)
if plan:
# Show success in debug mode
if st.session_state.show_debug:
st.success("β
JSON successfully extracted")
with st.expander("π Extracted Plan Preview", expanded=False):
st.json({
"subject": plan.get('subject'),
"total_weeks": plan.get('total_weeks'),
"weekly_schedule_length": len(plan.get('weekly_schedule', [])),
"first_week_focus": plan.get('weekly_schedule', [{}])[0].get('focus', 'N/A') if plan.get('weekly_schedule') else 'N/A'
})
# Post-process the plan
plan['generated_at'] = datetime.now().isoformat()
plan['total_days'] = days_available
plan['user_level'] = current_level
plan['intensity'] = intensity
# Initialize week tracking
for week in plan.get('weekly_schedule', []):
week['completed'] = week.get('completed', False)
week['tasks_completed'] = week.get('tasks_completed', 0)
week['completed_tasks_info'] = week.get('completed_tasks_info', [])
# Calculate total tasks
total_tasks = 0
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_tasks in week['daily_tasks']:
if isinstance(day_tasks, list):
total_tasks += len(day_tasks)
week['total_tasks'] = total_tasks
return plan
else:
if st.session_state.show_debug:
st.warning("β οΈ Could not extract valid JSON from AI response")
if not raw_text:
st.error("β Empty response from AI")
else:
st.info("Falling back to generic plan")
return create_generic_plan(subject, days_available)
except Exception as e:
error_msg = str(e)
st.error(f"β AI Generation Error: {error_msg}")
# Handle safety errors
if "safety" in error_msg.lower() or "finish_reason" in error_msg.lower():
st.warning("π **Safety restriction triggered.** Try a different subject or simpler request.")
if st.session_state.show_debug:
with st.expander("π Error Details", expanded=False):
st.exception(e)
return create_generic_plan(subject, days_available)
def generate_weekly_test(api_key, week_number, weekly_tasks, subject):
"""
Generate test for a specific week
"""
if not api_key:
return None
try:
genai.configure(api_key=api_key)
model = genai.GenerativeModel('gemini-2.5-flash')
prompt = f"""Create 5 multiple choice questions for Week {week_number} of {subject}.
Return valid JSON with this structure:
{{
"questions": [
{{
"question": "Clear, complete question?",
"options": {{
"A": "Complete option A text",
"B": "Complete option B text",
"C": "Complete option C text",
"D": "Complete option D text"
}},
"correct_answer": "A",
"explanation": "Brief explanation"
}}
]
}}
Make ALL options complete sentences. Include exactly 5 questions."""
response = model.generate_content(
prompt,
generation_config=genai.GenerationConfig(
max_output_tokens=1500,
temperature=0.7
)
)
test_text = response.text.strip()
st.session_state.raw_response = test_text # Store for debug
return extract_test_json(test_text)
except Exception as e:
return None
def calculate_progress_status(plan, completed_tasks, total_tasks, days_passed, total_days):
"""
Calculate if user is on track, behind, or ahead
"""
if total_tasks == 0:
return "Not Started"
task_completion_rate = completed_tasks / total_tasks
time_passed_ratio = days_passed / total_days if total_days > 0 else 0
if task_completion_rate >= time_passed_ratio + 0.1:
return "Ahead of Schedule"
elif task_completion_rate >= time_passed_ratio - 0.1:
return "On Track"
else:
return "Behind Schedule"
def calculate_weekly_streak(plan):
"""
Calculate current and longest weekly streak from completed weeks
"""
if not plan or 'weekly_schedule' not in plan:
return 0, 0
completed_weeks = []
for week in plan['weekly_schedule']:
if week.get('completed', False):
week_num = week['week']
completion_date = week.get('completion_date', '2000-01-01')
completed_weeks.append((week_num, completion_date))
if not completed_weeks:
return 0, 0
completed_weeks.sort(key=lambda x: x[0])
current_streak = 1
longest_streak = 1
temp_streak = 1
for i in range(1, len(completed_weeks)):
if completed_weeks[i][0] == completed_weeks[i-1][0] + 1:
temp_streak += 1
longest_streak = max(longest_streak, temp_streak)
if completed_weeks[i][0] >= st.session_state.current_week - 1:
current_streak = temp_streak
else:
temp_streak = 1
return current_streak, longest_streak
def get_motivational_message(progress_percentage, status, weekly_streak):
"""
Get motivational message based on progress and streak
"""
import random
messages = {
"Not Started": [
"π― Every expert was once a beginner. Start your journey today!",
"π The first step is always the hardest. You've got this!",
"π Your future self will thank you for starting today."
],
"Behind Schedule": [
"πͺ It's not about being perfect, it's about being consistent. Keep going!",
"π Falling behind is normal. Adjust your pace and continue forward.",
"π Progress is progress, no matter how small. Every step counts!"
],
"On Track": [
"β
Amazing! You're right on schedule. Keep up the great work!",
"π₯ Your consistency is paying off. You're doing fantastic!",
"π You're making steady progress toward your goal. Stay focused!"
],
"Ahead of Schedule": [
"β‘ You're crushing it! Consider challenging yourself with advanced topics.",
"ποΈ Ahead of schedule! Your dedication is inspiring.",
"π You're making rapid progress. Consider helping others or exploring deeper."
]
}
if status in messages:
return random.choice(messages[status])
if progress_percentage < 30:
return "π± You're planting the seeds of knowledge. Keep nurturing them!"
elif progress_percentage < 70:
return "π You're in the flow. Maintain this momentum!"
else:
return "ποΈ You're approaching the summit. The view will be worth it!"
def save_plan(plan, subject):
"""
Save study plan to JSON file
"""
import os
os.makedirs("data", exist_ok=True)
filename = f"data/{subject.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(filename, 'w', encoding='utf-8') as f:
json.dump(plan, f, indent=4, ensure_ascii=False)
return filename
def load_progress(subject):
"""
Load progress from file
"""
progress_file = f"data/{subject.replace(' ', '_')}_progress.json"
if os.path.exists(progress_file):
with open(progress_file, 'r', encoding='utf-8') as f:
try:
return json.load(f)
except:
return {}
return {}
def calculate_weekly_task_completion_with_flexibility(plan, subject):
"""
Calculate completed tasks per week with flexible completion dates
"""
weekly_completion = {}
if plan and plan.get('weekly_schedule'):
for week in plan['weekly_schedule']:
week_num = week['week']
total_tasks = 0
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_tasks in week['daily_tasks']:
if isinstance(day_tasks, list):
total_tasks += len(day_tasks)
weekly_completion[week_num] = {
'assigned_tasks': total_tasks,
'completed_in_week': 0,
'completed_elsewhere': 0,
'total_completed': 0
}
# Count completions
for week in plan['weekly_schedule']:
week_num = week['week']
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_idx, day_tasks in enumerate(week['daily_tasks']):
if isinstance(day_tasks, list):
for task_idx, task in enumerate(day_tasks):
task_key = f"task_week{week_num}_day{day_idx}_task{task_idx}_{subject}"
if st.session_state.get(task_key, False):
weekly_completion[week_num]['completed_in_week'] += 1
# Calculate totals
for week_num in weekly_completion:
weekly_completion[week_num]['total_completed'] = weekly_completion[week_num]['completed_in_week']
return weekly_completion
def save_progress(subject, progress_data):
"""
Save progress to file
"""
import os
os.makedirs("data", exist_ok=True)
progress_file = f"data/{subject.replace(' ', '_')}_progress.json"
with open(progress_file, 'w', encoding='utf-8') as f:
json.dump(progress_data, f, indent=4, ensure_ascii=False)
def analyze_study_patterns(daily_progress, study_days):
"""
Analyze study patterns and provide recommendations
"""
if not daily_progress:
return []
recommendations = []
try:
df = pd.DataFrame(daily_progress)
df['date'] = pd.to_datetime(df['date'], errors='coerce')
df = df.dropna(subset=['date'])
if len(df) < 2:
return []
date_range = (df['date'].max() - df['date'].min()).days + 1
study_days_count = len(df)
if date_range > 0:
study_frequency = (study_days_count / date_range) * 100
if study_frequency < 30:
recommendations.append(f"**π
Increase Study Frequency:** You're studying on {study_frequency:.0f}% of days. Try to study more regularly to build momentum.")
elif study_frequency > 80:
recommendations.append(f"**π― Excellent Consistency:** You're studying on {study_frequency:.0f}% of days! This consistency will lead to great results.")
# Hours analysis
if 'hours' in df.columns:
df['hours'] = pd.to_numeric(df['hours'], errors='coerce')
avg_hours = df['hours'].mean()
if avg_hours < 1:
recommendations.append(f"**β° Increase Study Duration:** Average {avg_hours:.1f} hours per session. Consider longer focused sessions for better learning.")
elif avg_hours > 4:
recommendations.append(f"**π§ Manage Study Load:** Average {avg_hours:.1f} hours per session is high. Consider breaking into shorter sessions to prevent burnout.")
else:
recommendations.append(f"**β
Optimal Study Duration:** Average {avg_hours:.1f} hours per session is perfect for focused learning.")
except Exception as e:
pass
return recommendations
def create_completion_certificate(subject, completion_percentage, total_tasks, completed_tasks, total_weeks, completed_weeks):
"""
Create a completion certificate HTML
"""
# Determine certificate level
if completion_percentage >= 95:
achievement = "with Honors"
color = "#FFD700"
message = "Exceptional Achievement"
elif completion_percentage >= 90:
achievement = "Successfully Completed"
color = "#C0C0C0"
message = "Excellent Completion"
elif completion_percentage >= 70:
achievement = "Good Progress"
color = "#3B82F6"
message = "Solid Progress"
else:
achievement = "In Progress"
color = "#10B981"
message = "Learning Journey"
current_date = datetime.now().strftime("%B %d, %Y")
certificate_html = f'''
Certificate - {subject}
π Certificate of Achievement
{message}
{subject}
Awarded to recognize outstanding dedication and completion of study program
{completion_percentage:.1f}%
Overall Progress
{completed_tasks}/{total_tasks}
Tasks Completed
{completed_weeks}/{total_weeks}
Weeks Completed
Achievement: {achievement}
Date: {current_date}
'''
return certificate_html
# ============================================
# πͺ MAIN APPLICATION LAYOUT - SIMPLIFIED
# ============================================
# Main header
st.markdown('π AI Study Planner Pro
', unsafe_allow_html=True)
st.markdown('Plan β Track β Achieve | Your personal AI study assistant
', unsafe_allow_html=True)
# API Key Status
if not st.session_state.api_key:
st.error("β οΈ **API Key Missing:** Please set GEMINI_API_KEY in Hugging Face Secrets.")
else:
st.success("β
**API Key Loaded:** Ready to generate AI-powered study plans!")
# Create tabs
tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([
"π― Define Goal",
"π
Study Plan",
"β
Track Progress",
"π Analytics",
"π§ͺ Test",
"π€ Export"
])
# ============================================
# TAB 1: DEFINE GOAL - WITH DROPDOWN OPTIONS
# ============================================
with tab1:
st.markdown('', unsafe_allow_html=True)
# Debug Mode Toggle
col_debug1, col_debug2 = st.columns([1, 3])
with col_debug1:
st.session_state.show_debug = st.checkbox("π Debug Mode", value=False, key="debug_mode_tab1")
# API Status
with col_debug2:
if not st.session_state.api_key:
st.error("β οΈ **API Key Missing:** Set GEMINI_API_KEY in Hugging Face Secrets")
else:
st.success("β
**API Key Loaded**")
# Test API Button with SAFETY FIX
if st.session_state.api_key and st.session_state.show_debug:
if st.button("Test API Connection", type="secondary", key="test_api_tab1"):
try:
genai.configure(api_key=st.session_state.api_key)
model = genai.GenerativeModel('gemini-2.5-flash')
# Use a safer, more neutral prompt
test_response = model.generate_content(
"Please respond with just the word 'Connected'",
generation_config=genai.GenerationConfig(
max_output_tokens=10,
temperature=0.1
)
)
# Safely get response text
if test_response and hasattr(test_response, 'text') and test_response.text:
st.success(f"β
**API Connected:** {test_response.text}")
else:
st.error("β No valid response from API")
except Exception as e:
st.error(f"β **API Error:** {str(e)}")
if "safety" in str(e).lower():
st.info("π **Safety restriction detected.** Try a different subject or rephrase your request.")
st.markdown("---")
# Option 1: Upload Existing Plan
st.info("π€ **Option 1: Upload Existing Plan**")
uploaded_file = st.file_uploader("Upload saved study plan (JSON)", type=['json'], key="upload_plan_tab1")
if uploaded_file is not None and not st.session_state.plan_loaded:
try:
plan = json.load(uploaded_file)
st.session_state.study_plan = plan
st.session_state.subject = plan['subject']
st.session_state.plan_loaded = True
st.success(f"β
Plan loaded: {plan['subject']}")
except Exception as e:
st.error(f"β Invalid plan file: {str(e)}")
st.markdown("---")
# Option 2: Create New Plan - WITH DROPDOWN + CUSTOM
st.info("π― **Option 2: Create New Plan**")
col1, col2 = st.columns([2, 1])
with col1:
# Subject selection: Dropdown OR Custom input
subject_options = [
"Select a subject or enter custom...",
"Data Science & Machine Learning",
"Python Programming",
"Web Development (Full Stack)",
"Artificial Intelligence",
"Cloud Computing (AWS/Azure/GCP)",
"Cybersecurity",
"Mobile App Development",
"Digital Marketing",
"Business Analytics",
"Language Learning (Spanish/French/English)",
"Project Management",
"Graphic Design",
"Game Development"
]
selected_option = st.selectbox(
"Choose a subject or enter custom:",
options=subject_options,
index=0,
key="subject_select_tab1"
)
if selected_option == "Select a subject or enter custom...":
subject = st.text_input(
"Or enter your own subject:",
placeholder="e.g., Quantum Computing, Ethical Hacking, Music Production...",
key="custom_subject_tab1"
)
else:
subject = selected_option
col1a, col2a = st.columns(2)
with col1a:
hours_per_day = st.slider(
"Hours per day:",
min_value=1,
max_value=8,
value=2,
key="hours_slider_tab1"
)
with col2a:
days_available = st.slider(
"Days available:",
min_value=7,
max_value=365,
value=60,
key="days_slider_tab1"
)
study_days = st.multiselect(
"Study days:",
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
default=["Mon", "Tue", "Wed", "Thu", "Fri"],
key="study_days_select_tab1"
)
with col2:
current_level = st.selectbox(
"Your level:",
["Beginner", "Intermediate", "Advanced"],
index=0,
key="level_select_tab1"
)
# Add study intensity
intensity = st.select_slider(
"Study intensity:",
options=["Casual", "Moderate", "Intensive"],
value="Moderate",
key="intensity_slider_tab1"
)
st.markdown("---")
# Generate Plan Button
if st.button("π Generate AI Study Plan", type="primary", use_container_width=True, key="generate_plan_btn_tab1"):
if not st.session_state.api_key:
st.error("β οΈ **API Key Required:** Please set GEMINI_API_KEY in Hugging Face Secrets.")
elif not subject or subject == "Select a subject or enter custom...":
st.error("β οΈ Please enter or select a subject")
elif not study_days:
st.error("β οΈ Please select at least one study day")
else:
with st.spinner("π€ AI is creating your personalized study plan..."):
try:
# Calculate learning quality
learning_quality = calculate_learning_quality(
days_available,
hours_per_day,
len(study_days)
)
# Create plan data with intensity
plan_data = {
'subject': subject,
'days_available': days_available,
'hours_per_day': hours_per_day,
'current_level': current_level,
'intensity': intensity,
'study_days': study_days
}
# Show plan data in debug mode
if st.session_state.show_debug:
with st.expander("π Plan Data Sent to AI", expanded=True):
st.json(plan_data)
# Generate plan
plan = generate_study_plan(st.session_state.api_key, **plan_data)
if plan:
st.session_state.study_plan = plan
st.session_state.subject = subject
st.session_state.current_week = 1
st.session_state.weekly_streak = 0
st.session_state.longest_weekly_streak = 0
st.session_state.plan_loaded = True
st.success("β
Study plan generated successfully!")
st.balloons()
# Show plan summary
st.info(f"π
**Schedule:** {len(study_days)} days/week ({', '.join(study_days)}) Γ {hours_per_day} hours/day")
st.info(f"π **Duration:** {plan.get('total_weeks', 0)} weeks ({days_available} days)")
# Show learning quality
if learning_quality['is_optimal']:
st.success(f"π― **Learning Quality:** {learning_quality['quality_score']:.0f}/100 (Excellent)")
else:
st.warning(f"π **Learning Quality:** {learning_quality['quality_score']:.0f}/100 (Needs improvement)")
st.caption(f"Coverage: {learning_quality['coverage_percentage']:.0f}% | Total hours: {learning_quality['total_hours']}h")
else:
st.error("β Failed to generate plan. Check debug mode for details.")
except Exception as e:
st.error(f"β Error: {str(e)}")
if st.session_state.show_debug:
with st.expander("π Error Details", expanded=True):
st.exception(e)
# Debug Info Section
if st.session_state.show_debug and st.session_state.raw_response:
st.markdown("---")
st.subheader("π Debug Information")
col_debug_a, col_debug_b = st.columns(2)
with col_debug_a:
st.metric("Raw Response Length", f"{len(st.session_state.raw_response)} chars")
with col_debug_b:
if st.session_state.study_plan:
plan_type = "AI Generated" if st.session_state.study_plan.get('generated_at') else "Generic"
st.metric("Plan Type", plan_type)
with st.expander("π Raw AI Response", expanded=False):
st.text_area("Full Response", st.session_state.raw_response, height=300, key="raw_response_display_tab1")
if st.session_state.study_plan:
with st.expander("π Parsed Plan Structure", expanded=False):
st.json(st.session_state.study_plan)
# ============================================
# TAB 2: STUDY PLAN
# ============================================
with tab2:
if st.session_state.study_plan:
plan = st.session_state.study_plan
subject = st.session_state.subject
study_days = plan.get('study_days', ["Mon", "Tue", "Wed", "Thu", "Fri"])
st.markdown(f'', unsafe_allow_html=True)
# Calculate progress metrics
total_tasks = 0
completed_tasks = 0
for week in plan.get('weekly_schedule', []):
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_idx, day_tasks in enumerate(week['daily_tasks']):
if isinstance(day_tasks, list):
total_tasks += len(day_tasks)
for task_idx in range(len(day_tasks)):
task_key = f"task_week{week['week']}_day{day_idx}_task{task_idx}_{subject}"
if st.session_state.get(task_key, False):
completed_tasks += 1
progress_percentage = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
start_date_str = plan.get('start_date', datetime.now().strftime("%Y-%m-%d"))
try:
start_date = datetime.strptime(start_date_str, "%Y-%m-%d")
days_passed = max(1, (datetime.now() - start_date).days + 1)
except:
days_passed = 1
total_days = plan.get('total_days', 60)
status = calculate_progress_status(plan, completed_tasks, total_tasks, days_passed, total_days)
current_streak, longest_streak = calculate_weekly_streak(plan)
st.session_state.weekly_streak = current_streak
st.session_state.longest_weekly_streak = longest_streak
status_class = {
"Not Started": "status-behind",
"Behind Schedule": "status-behind",
"On Track": "status-on-track",
"Ahead of Schedule": "status-ahead"
}.get(status, "status-on-track")
# Display metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
weeks = plan.get('total_weeks', 0)
st.metric("Total Weeks", weeks)
with col2:
st.metric("Study Days/Week", len(study_days))
with col3:
if current_streak > 0:
st.markdown(f"""
π₯ {current_streak} Week Streak
Longest: {longest_streak} weeks
""", unsafe_allow_html=True)
else:
st.metric("Weekly Streak", "0 weeks")
with col4:
daily_hours = plan.get('daily_hours', 2)
st.metric("Daily Hours", f"{daily_hours}h")
st.info(f"π
Your study schedule: {len(study_days)} days per week ({', '.join(study_days)})")
# Progress display
st.markdown(f"""
Progress Status
Overall Progress: {progress_percentage:.1f}%
Weekly Streak: {current_streak} weeks β’ Longest: {longest_streak} weeks
{status}
{completed_tasks} of {total_tasks} tasks completed | Day {min(days_passed, total_days)} of {total_days}
""", unsafe_allow_html=True)
# Motivation
motivation = get_motivational_message(progress_percentage, status, current_streak)
st.info(f"π‘ {motivation}")
# Study Tips
st.subheader("π‘ Simple Study Tips")
simple_tips = [
"π― **Set Daily Goals**: Know what you want to achieve each day",
"β° **Study in Short Bursts**: 25-30 minutes of focused study",
"π **Review Regularly**: Spend 5 minutes reviewing yesterday's material",
"πͺ **Practice Daily**: Consistency is more important than perfection",
"π§ **Take Breaks**: Stand up and stretch every hour"
]
tips_cols = st.columns(2)
for i, tip in enumerate(simple_tips):
with tips_cols[i % 2]:
st.markdown(f'{tip}
', unsafe_allow_html=True)
# Topic Allocation Chart
if 'topics_allocation' in plan:
st.subheader("π Topic Allocation")
topics = plan['topics_allocation']
chart_data = []
for topic, value in topics.items():
try:
num_value = float(value)
if num_value > 0:
chart_data.append({"Topic": topic, "Percentage": num_value})
except:
continue
if chart_data:
df = pd.DataFrame(chart_data)
fig = px.pie(df, values='Percentage', names='Topic',
title=f"Time Allocation for {subject}", hole=0.3)
st.plotly_chart(fig, use_container_width=True)
# Weekly Schedule
st.subheader("π Weekly Schedule")
current_week = st.selectbox(
"Jump to week:",
options=list(range(1, plan.get('total_weeks', 1) + 1)),
index=st.session_state.current_week - 1,
key="week_selector"
)
st.session_state.current_week = current_week
if plan.get('weekly_schedule'):
week_idx = current_week - 1
if week_idx < len(plan['weekly_schedule']):
week = plan['weekly_schedule'][week_idx]
with st.expander(f"**Week {week['week']}: {week.get('focus', 'Weekly Focus')}**", expanded=True):
# Week Focus Box
st.markdown(f"""
π Week {week['week']} Focus
{week.get('focus', 'Weekly learning objectives')}
""", unsafe_allow_html=True)
col1, col2 = st.columns([2, 1])
with col1:
st.markdown("### π Objectives")
if 'objectives' in week and week['objectives']:
for obj in week['objectives']:
st.markdown(f"β’ **{obj}**")
st.markdown("### π― Milestone")
if 'milestone' in week:
st.info(f"**{week['milestone']}**")
if week.get('completed') and week.get('completion_date'):
st.success(f"β
**Completed on:** {week['completion_date']}")
with col2:
week_tasks = week['total_tasks']
completed_week_tasks = 0
# Count completed tasks for this week
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_idx, day_tasks in enumerate(week['daily_tasks']):
if isinstance(day_tasks, list):
for task_idx in range(len(day_tasks)):
task_key = f"task_week{week['week']}_day{day_idx}_task{task_idx}_{subject}"
if st.session_state.get(task_key, False):
completed_week_tasks += 1
week_progress = (completed_week_tasks / week_tasks * 100) if week_tasks > 0 else 0
week_completed = (completed_week_tasks == week_tasks)
if week_completed:
st.success("β
**Week Completed**")
if week.get('completion_date'):
st.caption(f"Completed on: {week['completion_date']}")
else:
st.metric("Week Progress", f"{week_progress:.0f}%")
if st.button(f"Mark Week {week['week']} Complete", key=f"complete_week_{week['week']}"):
week['completed'] = True
week['completion_date'] = datetime.now().strftime("%Y-%m-%d")
week['tasks_completed'] = completed_week_tasks
new_streak, new_longest = calculate_weekly_streak(plan)
st.session_state.weekly_streak = new_streak
st.session_state.longest_weekly_streak = max(st.session_state.longest_weekly_streak, new_longest)
save_plan(plan, subject)
progress_data = load_progress(subject)
if progress_data:
progress_data['completed_weeks'] = [w['week'] for w in plan.get('weekly_schedule', []) if w.get('completed', False)]
progress_data['weekly_streak'] = new_streak
progress_data['longest_weekly_streak'] = max(progress_data.get('longest_weekly_streak', 0), new_longest)
progress_data['last_updated'] = datetime.now().isoformat()
save_progress(subject, progress_data)
st.success(f"Week {week['week']} marked as complete!")
st.balloons()
st.rerun()
# Week Summary
if week.get('week_summary'):
st.markdown(f"""
π Week Summary
{week['week_summary']}
""", unsafe_allow_html=True)
st.markdown("---")
st.markdown(f"### π Tasks for Week {week['week']}")
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
day_focus = week.get('day_focus', [])
for day_idx, day_tasks in enumerate(week['daily_tasks']):
if isinstance(day_tasks, list) and day_idx < len(study_days):
day_name = study_days[day_idx]
st.markdown(f"#### {day_name}")
# Show day focus
if day_idx < len(day_focus):
st.markdown(f"""
Focus: {day_focus[day_idx]}
""", unsafe_allow_html=True)
# Show each task
for task_idx, task in enumerate(day_tasks):
task_key = f"task_week{week['week']}_day{day_idx}_task{task_idx}_{subject}"
if task_key not in st.session_state:
st.session_state[task_key] = False
task_class = "completed" if st.session_state[task_key] else ""
# Task container
col_check, col_desc = st.columns([1, 20])
with col_check:
is_completed = st.checkbox(
"",
value=st.session_state[task_key],
key=f"checkbox_{task_key}",
label_visibility="collapsed"
)
with col_desc:
st.markdown(f"""
{task}
""", unsafe_allow_html=True)
if is_completed != st.session_state[task_key]:
st.session_state[task_key] = is_completed
if is_completed:
today = datetime.now().strftime("%Y-%m-%d")
st.session_state.task_completion_dates[task_key] = today
# Update week progress
completed_week_tasks = 0
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for d_idx, d_tasks in enumerate(week['daily_tasks']):
if isinstance(d_tasks, list):
for t_idx in range(len(d_tasks)):
tk = f"task_week{week['week']}_day{d_idx}_task{t_idx}_{subject}"
if st.session_state.get(tk, False):
completed_week_tasks += 1
week['tasks_completed'] = completed_week_tasks
week['completed'] = (completed_week_tasks == week['total_tasks'])
if week['completed'] and not week.get('completion_date'):
week['completion_date'] = today
new_streak, new_longest = calculate_weekly_streak(plan)
st.session_state.weekly_streak = new_streak
st.session_state.longest_weekly_streak = max(st.session_state.longest_weekly_streak, new_longest)
save_plan(plan, subject)
progress_data = load_progress(subject)
if progress_data:
all_completed = 0
for w in plan.get('weekly_schedule', []):
if 'daily_tasks' in w and isinstance(w['daily_tasks'], list):
for d_idx, d_tasks in enumerate(w['daily_tasks']):
if isinstance(d_tasks, list):
for t_idx in range(len(d_tasks)):
tk = f"task_week{w['week']}_day{d_idx}_task{t_idx}_{subject}"
if st.session_state.get(tk, False):
all_completed += 1
progress_data['completed_tasks'] = all_completed
progress_data['completed_weeks'] = [w['week'] for w in plan.get('weekly_schedule', []) if w.get('completed', False)]
progress_data['weekly_streak'] = st.session_state.weekly_streak
progress_data['longest_weekly_streak'] = max(progress_data.get('longest_weekly_streak', 0), st.session_state.longest_weekly_streak)
progress_data['last_updated'] = datetime.now().isoformat()
save_progress(subject, progress_data)
st.rerun()
st.markdown("---")
else:
st.info("No tasks defined for this week")
else:
st.info("π **Define your learning goal first in the 'Define Goal' tab!**")
# ============================================
# TAB 3: TRACK PROGRESS
# ============================================
with tab3:
if st.session_state.study_plan:
subject = st.session_state.subject
progress_data = load_progress(subject)
plan = st.session_state.study_plan
study_days = plan.get('study_days', ["Mon", "Tue", "Wed", "Thu", "Fri"])
st.markdown(f'', unsafe_allow_html=True)
# Calculate progress
total_tasks = 0
completed_tasks = 0
for week in plan.get('weekly_schedule', []):
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_idx, day_tasks in enumerate(week['daily_tasks']):
if isinstance(day_tasks, list):
total_tasks += len(day_tasks)
for task_idx in range(len(day_tasks)):
task_key = f"task_week{week['week']}_day{day_idx}_task{task_idx}_{subject}"
if st.session_state.get(task_key, False):
completed_tasks += 1
progress_percentage = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
weekly_completion = calculate_weekly_task_completion_with_flexibility(plan, subject)
current_streak, longest_streak = calculate_weekly_streak(plan)
# Display metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Overall Progress", f"{progress_percentage:.1f}%")
with col2:
st.metric("Tasks Completed", f"{completed_tasks}/{total_tasks}")
with col3:
completed_weeks = sum(1 for week in plan.get('weekly_schedule', []) if week.get('completed', False))
total_weeks = plan.get('total_weeks', 0)
st.metric("Weeks Completed", f"{completed_weeks}/{total_weeks}")
with col4:
if current_streak > 0:
st.markdown(f"""
π₯ {current_streak} Week Streak
Longest: {longest_streak} weeks
""", unsafe_allow_html=True)
else:
st.metric("Weekly Streak", "0 weeks")
# Weekly Task Completion Chart
if weekly_completion:
st.subheader("π Weekly Task Completion")
weeks_list = sorted(weekly_completion.keys())
completed_in_week = [weekly_completion[w]['completed_in_week'] for w in weeks_list]
total_assigned = [weekly_completion[w]['assigned_tasks'] for w in weeks_list]
df_tasks = pd.DataFrame({
'Week': [f"Week {w}" for w in weeks_list],
'Completed': completed_in_week,
'Total Assigned': total_assigned,
'Completion %': [(completed_in_week[i] / total_assigned[i] * 100) if total_assigned[i] > 0 else 0 for i in range(len(weeks_list))]
})
fig = go.Figure()
fig.add_trace(go.Bar(
x=df_tasks['Week'],
y=df_tasks['Completed'],
name='Completed Tasks',
marker_color='#10B981',
text=df_tasks['Completed'],
textposition='inside'
))
remaining = [total_assigned[i] - completed_in_week[i] for i in range(len(weeks_list))]
fig.add_trace(go.Bar(
x=df_tasks['Week'],
y=remaining,
name='Remaining Tasks',
marker_color='#E5E7EB',
text=remaining,
textposition='inside'
))
fig.update_layout(
title='Weekly Task Completion',
barmode='stack',
yaxis_title='Number of Tasks',
showlegend=True,
hovermode='x unified'
)
st.plotly_chart(fig, use_container_width=True)
st.info(f"π **Total tasks completed:** {sum(completed_in_week)} out of {sum(total_assigned)}")
# Weekly Progress Overview
st.subheader("π Weekly Progress Overview")
if plan.get('weekly_schedule'):
weekly_progress = []
for week in plan['weekly_schedule']:
week_num = week['week']
week_tasks = week['total_tasks']
week_completion = weekly_completion.get(week_num, {'total_completed': 0, 'assigned_tasks': week_tasks})
completion_percentage = (week_completion['total_completed'] / week_tasks * 100) if week_tasks > 0 else 0
weekly_progress.append({
"Week": week_num,
"Week_Label": f"Week {week_num}",
"Progress": completion_percentage,
"Completed": week.get('completed', False),
"Completion Date": week.get('completion_date', 'Not completed'),
"Tasks Completed": week_completion['total_completed'],
"Total Tasks": week_tasks
})
if weekly_progress:
df_weekly = pd.DataFrame(weekly_progress)
df_weekly = df_weekly.sort_values('Week')
fig3 = px.bar(df_weekly, x='Week_Label', y='Progress',
title='Weekly Completion Percentage',
color='Completed',
color_discrete_map={True: '#10B981', False: '#3B82F6'},
hover_data=['Tasks Completed', 'Total Tasks', 'Completion Date'])
fig3.add_hline(y=100, line_dash="dash", line_color="green",
annotation_text="Complete",
annotation_position="bottom right")
st.plotly_chart(fig3, use_container_width=True)
with st.expander("π Weekly Completion Details", expanded=False):
st.dataframe(df_weekly[['Week', 'Tasks Completed', 'Total Tasks', 'Progress', 'Completed', 'Completion Date']],
use_container_width=True)
# Daily Progress Log
st.subheader("π Daily Progress Log")
with st.form("daily_log_form"):
today = datetime.now().strftime("%Y-%m-%d")
st.write(f"**Date:** {today}")
col1, col2, col3 = st.columns(3)
with col1:
hours_studied = st.number_input("Hours studied today:",
min_value=0.0, max_value=12.0,
value=2.0, step=0.5)
with col2:
study_hour = st.selectbox(
"Study hour of the day:",
options=["1st hour (00:00-01:00)", "2nd hour (01:00-02:00)", "3rd hour (02:00-03:00)",
"4th hour (03:00-04:00)", "5th hour (04:00-05:00)", "6th hour (05:00-06:00)",
"7th hour (06:00-07:00)", "8th hour (07:00-08:00)", "9th hour (08:00-09:00)",
"10th hour (09:00-10:00)", "11th hour (10:00-11:00)", "12th hour (11:00-12:00)",
"13th hour (12:00-13:00)", "14th hour (13:00-14:00)", "15th hour (14:00-15:00)",
"16th hour (15:00-16:00)", "17th hour (16:00-17:00)", "18th hour (17:00-18:00)",
"19th hour (18:00-19:00)", "20th hour (19:00-20:00)", "21st hour (20:00-21:00)",
"22nd hour (21:00-22:00)", "23rd hour (22:00-23:00)", "24th hour (23:00-00:00)"],
index=8,
help="Select the hour when you studied"
)
with col3:
mood = st.select_slider("Today's study mood:",
options=["π«", "π", "π", "π", "π", "π€©"],
value="π")
topics_covered = st.text_input("Topics covered today:",
placeholder=f"e.g., {subject} concepts, exercises...")
achievements = st.text_area("Key achievements:",
placeholder="What did you learn or accomplish today?")
challenges = st.text_area("Challenges faced:",
placeholder="What was difficult? How did you overcome it?")
if st.form_submit_button("πΎ Save Today's Progress", use_container_width=True):
daily_log = {
'date': today,
'hours': hours_studied,
'study_hour': study_hour,
'mood': mood,
'topics': topics_covered,
'achievements': achievements,
'challenges': challenges
}
if 'daily_progress' not in progress_data:
progress_data['daily_progress'] = []
# Update or add entry
existing_entry = False
for i, entry in enumerate(progress_data['daily_progress']):
if entry.get('date') == today:
progress_data['daily_progress'][i] = daily_log
existing_entry = True
break
if not existing_entry:
progress_data['daily_progress'].append(daily_log)
progress_data['completed_tasks'] = completed_tasks
progress_data['completed_weeks'] = [w['week'] for w in plan.get('weekly_schedule', []) if w.get('completed', False)]
progress_data['weekly_streak'] = current_streak
progress_data['longest_weekly_streak'] = max(progress_data.get('longest_weekly_streak', 0), longest_streak)
progress_data['last_updated'] = datetime.now().isoformat()
save_progress(subject, progress_data)
st.success("β
Progress saved successfully!")
st.rerun()
# Progress History
if progress_data.get('daily_progress'):
st.subheader("π
Progress History")
try:
df = pd.DataFrame(progress_data['daily_progress'])
if not df.empty:
display_df = df.copy()
if 'date' in display_df.columns:
try:
display_df['Date'] = pd.to_datetime(display_df['date']).dt.strftime('%Y-%m-%d')
except:
display_df['Date'] = display_df['date']
display_cols = []
if 'Date' in display_df.columns:
display_cols.append('Date')
if 'hours' in display_df.columns:
display_df['Hours'] = display_df['hours']
display_cols.append('Hours')
if 'study_hour' in display_df.columns:
display_df['Study Hour'] = display_df['study_hour']
display_cols.append('Study Hour')
if 'mood' in display_df.columns:
display_df['Mood'] = display_df['mood']
display_cols.append('Mood')
if 'topics' in display_df.columns:
display_df['Topics'] = display_df['topics']
display_cols.append('Topics')
if 'achievements' in display_df.columns:
display_df['Achievements'] = display_df['achievements']
display_cols.append('Achievements')
if 'challenges' in display_df.columns:
display_df['Challenges'] = display_df['challenges']
display_cols.append('Challenges')
if display_cols:
st.dataframe(display_df[display_cols], use_container_width=True)
else:
st.warning("No valid data available to display.")
except Exception as e:
st.warning(f"Could not display progress history: {str(e)}")
else:
st.info("π **Generate a study plan first to track progress!**")
# ============================================
# TAB 4: ANALYTICS
# ============================================
with tab4:
if st.session_state.study_plan:
st.markdown('', unsafe_allow_html=True)
subject = st.session_state.subject
progress_data = load_progress(subject)
plan = st.session_state.study_plan
study_days = plan.get('study_days', ["Mon", "Tue", "Wed", "Thu", "Fri"])
daily_progress = progress_data.get('daily_progress', [])
study_days_logged = len(daily_progress)
# Calculate completed tasks
completed_tasks = 0
total_tasks = 0
for week in plan.get('weekly_schedule', []):
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_idx, day_tasks in enumerate(week['daily_tasks']):
if isinstance(day_tasks, list):
total_tasks += len(day_tasks)
for task_idx in range(len(day_tasks)):
task_key = f"task_week{week['week']}_day{day_idx}_task{task_idx}_{subject}"
if st.session_state.get(task_key, False):
completed_tasks += 1
completed_weeks = sum(1 for week in plan.get('weekly_schedule', []) if week.get('completed', False))
total_weeks = plan.get('total_weeks', 0)
current_streak = progress_data.get('weekly_streak', 0)
longest_streak = progress_data.get('longest_weekly_streak', 0)
if daily_progress:
try:
col1, col2, col3, col4 = st.columns(4)
with col1:
if daily_progress:
df = pd.DataFrame(daily_progress)
if 'hours' in df.columns:
df['hours'] = pd.to_numeric(df['hours'], errors='coerce')
total_hours = df['hours'].sum()
else:
total_hours = 0
else:
total_hours = 0
st.metric("Total Hours", f"{total_hours:.1f}")
with col2:
st.metric("Study Days Logged", study_days_logged)
with col3:
st.metric("Tasks Completed", f"{completed_tasks}/{total_tasks}")
with col4:
st.metric("Weekly Streak", f"{current_streak} weeks")
st.subheader("π Study Patterns Analysis")
if daily_progress and len(daily_progress) > 1:
df = pd.DataFrame(daily_progress)
df['date'] = pd.to_datetime(df['date'], errors='coerce')
df = df.dropna(subset=['date'])
if len(df) > 1:
if 'study_hour' in df.columns:
st.subheader("π Preferred Study Hours")
def extract_hour(hour_str):
if 'hour' in hour_str:
try:
hour_num = int(''.join(filter(str.isdigit, hour_str.split('hour')[0].strip())))
return hour_num
except:
return 0
return 0
df['hour_num'] = df['study_hour'].apply(extract_hour)
fig_hour = go.Figure()
fig_hour.add_trace(go.Scatter(
x=df['date'],
y=df['hour_num'],
mode='lines+markers',
name='Study Hour',
line=dict(color='#3B82F6', width=2),
marker=dict(size=8),
text=df['study_hour'],
hoverinfo='text+y'
))
fig_hour.update_layout(
title='Study Hours Over Time',
xaxis_title='Date',
yaxis_title='Study Hour (1-24)',
yaxis=dict(tickmode='linear', dtick=1),
hovermode='x unified',
showlegend=True
)
st.plotly_chart(fig_hour, use_container_width=True)
if not df.empty and 'study_hour' in df.columns:
common_hour = df['study_hour'].mode()
if not common_hour.empty:
st.info(f"**Most frequent study time:** {common_hour.iloc[0]}")
if 'mood' in df.columns:
st.subheader("π Study Mood Analysis")
mood_counts = df['mood'].value_counts().reset_index()
mood_counts.columns = ['Mood', 'Count']
fig_mood = px.pie(mood_counts, values='Count', names='Mood',
title='Mood Distribution During Study Sessions',
hole=0.3)
st.plotly_chart(fig_mood, use_container_width=True)
st.subheader("π‘ Personalized Recommendations")
recommendations = []
if daily_progress:
pattern_recommendations = analyze_study_patterns(daily_progress, study_days)
recommendations.extend(pattern_recommendations)
progress_percentage = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
if progress_percentage < 30:
recommendations.append("**π± You're just getting started!** Focus on building consistent weekly study habits.")
elif progress_percentage < 70:
recommendations.append("**π Great momentum!** You're making solid progress. Keep up the weekly consistency!")
else:
recommendations.append("**π Excellent progress!** You're approaching completion. Stay focused on your final goals!")
if current_streak > 0:
if current_streak >= 5:
recommendations.append(f"π₯ **Incredible {current_streak}-week streak!** You've mastered consistent study habits!")
elif current_streak >= 3:
recommendations.append(f"π― **Great {current_streak}-week streak!** You're building powerful momentum!")
else:
recommendations.append(f"π **Good start with {current_streak} week streak!** Keep going to build a longer streak!")
else:
recommendations.append("β³ **Start a streak!** Complete a full week to begin your weekly streak journey.")
if recommendations:
for rec in recommendations:
st.info(rec)
else:
st.success("**Excellent!** Your study habits are on track. Keep up the good work!")
st.subheader("β
Success Metrics Check")
success_data = []
success_data.append({
'Metric': 'Task completion progress',
'Status': 'β
Excellent' if progress_percentage >= 80 else 'π‘ Good' if progress_percentage >= 50 else 'β³ Needs work',
'Value': f'{progress_percentage:.0f}%'
})
week_completion_rate = (completed_weeks / total_weeks * 100) if total_weeks > 0 else 0
success_data.append({
'Metric': 'Weekly completion rate',
'Status': 'β
Excellent' if week_completion_rate >= 80 else 'π‘ Good' if week_completion_rate >= 50 else 'β³ Needs work',
'Value': f'{week_completion_rate:.0f}%'
})
df_success = pd.DataFrame(success_data)
st.dataframe(df_success, use_container_width=True)
except Exception as e:
st.warning(f"Could not generate analytics: {str(e)}")
else:
st.info("π **Log your daily progress in the 'Track Progress' tab to unlock analytics!**")
else:
st.info("π **Generate a study plan first to see analytics!**")
# ============================================
# TAB 5: TESTS - FIXED DUPLICATE BUTTON ERROR
# ============================================
with tab5:
if st.session_state.study_plan:
plan = st.session_state.study_plan
subject = st.session_state.subject
st.markdown(f'', unsafe_allow_html=True)
# Test Selection View
if not st.session_state.current_test:
st.info("Take weekly tests to assess your understanding.")
if plan.get('weekly_schedule'):
for week in plan['weekly_schedule']:
week_num = week['week']
# Calculate completion
week_tasks = week['total_tasks']
completed_week_tasks = 0
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_idx, day_tasks in enumerate(week['daily_tasks']):
if isinstance(day_tasks, list):
for task_idx in range(len(day_tasks)):
task_key = f"task_week{week_num}_day{day_idx}_task{task_idx}_{subject}"
if st.session_state.get(task_key, False):
completed_week_tasks += 1
completion_percentage = (completed_week_tasks / week_tasks * 100) if week_tasks > 0 else 0
test_available = completion_percentage >= 50
with st.container():
col1, col2, col3 = st.columns([3, 1, 1])
with col1:
st.markdown(f"**Week {week_num}:** {week.get('focus', '')[:50]}...")
st.caption(f"Completion: {completion_percentage:.0f}% β’ Tasks: {completed_week_tasks}/{week_tasks}")
with col2:
test_key = f"week_{week_num}_test"
test_taken = test_key in st.session_state.test_results
if test_taken:
score = st.session_state.test_results[test_key]['percentage']
st.metric("", f"{score:.0f}%", label_visibility="collapsed")
with col3:
if test_available:
if st.button(f"π Start Test",
key=f"start_test_week_{week_num}_{subject.replace(' ', '_')}",
use_container_width=True):
with st.spinner(f"Generating test for Week {week_num}..."):
test_data = generate_weekly_test(
st.session_state.api_key,
week_num,
week.get('daily_tasks', []),
subject
)
if test_data and 'questions' in test_data:
st.session_state.current_test = week_num
st.session_state.test_questions = test_data['questions']
st.session_state.test_completed = False
st.session_state.user_test_answers = {}
st.rerun()
else:
st.error("Failed to generate test. Try again.")
else:
# FIXED: Added unique key and disabled properly
st.button(f"π Locked",
key=f"locked_test_week_{week_num}_{subject.replace(' ', '_')}",
disabled=True,
use_container_width=True)
if completion_percentage < 50:
st.caption(f"Complete 50% to unlock")
st.markdown("---")
# Test Taking View
elif st.session_state.current_test and not st.session_state.test_completed:
week_num = st.session_state.current_test
test_questions = st.session_state.test_questions
st.markdown(f"### π Week {week_num} Test")
st.info(f"Answer all {len(test_questions)} questions. Click on your chosen answer.")
for i, question in enumerate(test_questions):
with st.container():
st.markdown(f"**Q{i+1}. {question['question']}**")
current_answer = st.session_state.user_test_answers.get(str(i), "")
# Create 2 columns for options
col_a, col_b = st.columns(2)
with col_a:
# Option A and B
for option in ['A', 'B']:
option_text = question['options'][option]
is_selected = (current_answer == option)
if st.button(
f"{option}. {option_text[:80]}..." if len(option_text) > 80 else f"{option}. {option_text}",
key=f"test_q{week_num}_{i}_opt{option}",
use_container_width=True,
type="primary" if is_selected else "secondary"
):
st.session_state.user_test_answers[str(i)] = option
st.rerun()
with col_b:
# Option C and D
for option in ['C', 'D']:
option_text = question['options'][option]
is_selected = (current_answer == option)
if st.button(
f"{option}. {option_text[:80]}..." if len(option_text) > 80 else f"{option}. {option_text}",
key=f"test_q{week_num}_{i}_opt{option}",
use_container_width=True,
type="primary" if is_selected else "secondary"
):
st.session_state.user_test_answers[str(i)] = option
st.rerun()
st.markdown("---")
# Submit button
col_submit1, col_submit2, col_submit3 = st.columns([1, 2, 1])
with col_submit2:
if st.button("π€ Submit Test",
type="primary",
key=f"submit_test_week_{week_num}",
use_container_width=True):
st.session_state.test_completed = True
st.rerun()
# Test Results View
elif st.session_state.test_completed:
week_num = st.session_state.current_test
test_questions = st.session_state.test_questions
user_answers = st.session_state.user_test_answers
# Calculate results
correct_count = 0
for i, question in enumerate(test_questions):
if user_answers.get(str(i)) == question['correct_answer']:
correct_count += 1
score_percentage = (correct_count / len(test_questions)) * 100
# Store results
test_key = f"week_{week_num}_test"
st.session_state.test_results[test_key] = {
'score': correct_count,
'total': len(test_questions),
'percentage': score_percentage
}
# Display results
st.success(f"## π― Test Score: {score_percentage:.1f}%")
st.info(f"**{correct_count} out of {len(test_questions)} correct**")
# Performance feedback
if score_percentage >= 80:
st.balloons()
st.success("**Excellent!** You have a strong understanding of this week's material.")
elif score_percentage >= 60:
st.success("**Good job!** You understand the main concepts.")
else:
st.warning("**Review needed.** Consider revisiting this week's material.")
with st.expander("π Review Answers", expanded=False):
for i, question in enumerate(test_questions):
user_answer = user_answers.get(str(i), "Not answered")
is_correct = user_answer == question['correct_answer']
st.markdown(f"**Q{i+1}. {question['question']}**")
# Show user's answer with color
if user_answer in question['options']:
user_answer_text = f"{user_answer}. {question['options'][user_answer]}"
if is_correct:
st.success(f"β
**Your answer:** {user_answer_text}")
else:
st.error(f"β **Your answer:** {user_answer_text}")
else:
st.warning("βΈοΈ **Your answer:** Not answered")
# Show correct answer
correct_answer_text = f"{question['correct_answer']}. {question['options'][question['correct_answer']]}"
st.info(f"π **Correct answer:** {correct_answer_text}")
# Show explanation
if question.get('explanation'):
st.markdown(f"π‘ **Explanation:** {question['explanation']}")
st.markdown("---")
# Back button
if st.button("β Back to Test List",
key=f"back_from_test_{week_num}",
use_container_width=True):
st.session_state.current_test = None
st.session_state.test_completed = False
st.session_state.test_questions = []
st.session_state.user_test_answers = {}
st.rerun()
else:
st.info("π **Generate a study plan first!**")
# ============================================
# TAB 6: EXPORT - FIXED MISSING FLEXIBLE_COMPLETION
# ============================================
with tab6:
st.markdown('', unsafe_allow_html=True)
if st.session_state.study_plan:
plan = st.session_state.study_plan
subject = st.session_state.subject
st.info("πΎ **Download your study plan to continue later**")
st.write("Save your progress and come back anytime to continue where you left off!")
export_plan = plan.copy()
# Update completed tasks info
if export_plan.get('weekly_schedule'):
for week in export_plan['weekly_schedule']:
week_num = week['week']
completed_tasks_info = []
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_idx, day_tasks in enumerate(week['daily_tasks']):
if isinstance(day_tasks, list):
for task_idx, task in enumerate(day_tasks):
task_key = f"task_week{week_num}_day{day_idx}_task{task_idx}_{subject}"
if st.session_state.get(task_key, False):
completion_date = st.session_state.task_completion_dates.get(task_key, datetime.now().strftime("%Y-%m-%d"))
completed_tasks_info.append({
'day_idx': day_idx,
'task_idx': task_idx,
'task': task,
'completion_date': completion_date
})
week['completed_tasks_info'] = completed_tasks_info
week['tasks_completed'] = len(completed_tasks_info)
week['completed'] = (len(completed_tasks_info) == week['total_tasks'])
if week['completed'] and not week.get('completion_date'):
dates = [t['completion_date'] for t in completed_tasks_info if t.get('completion_date')]
if dates:
week['completion_date'] = max(dates)
export_plan['task_completion_dates'] = st.session_state.task_completion_dates
export_plan['flexible_completion'] = st.session_state.flexible_completion
export_plan['exported_at'] = datetime.now().isoformat()
# Add daily progress
progress_data = load_progress(subject)
if progress_data and 'daily_progress' in progress_data:
export_plan['daily_progress'] = progress_data['daily_progress']
json_str = json.dumps(export_plan, indent=2, ensure_ascii=False)
st.download_button(
label="π Download Study Plan (JSON)",
data=json_str,
file_name=f"study_plan_{subject.replace(' ', '_')}.json",
mime="application/json",
use_container_width=True,
help="Download your complete study plan with all your progress and streak information"
)
st.markdown("---")
st.subheader("π Completion Certificate")
# Calculate completion metrics
total_tasks = 0
completed_tasks = 0
for week in plan.get('weekly_schedule', []):
if 'daily_tasks' in week and isinstance(week['daily_tasks'], list):
for day_idx, day_tasks in enumerate(week['daily_tasks']):
if isinstance(day_tasks, list):
total_tasks += len(day_tasks)
for task_idx in range(len(day_tasks)):
task_key = f"task_week{week['week']}_day{day_idx}_task{task_idx}_{subject}"
if st.session_state.get(task_key, False):
completed_tasks += 1
completed_weeks = sum(1 for week in plan.get('weekly_schedule', []) if week.get('completed', False))
total_weeks = plan.get('total_weeks', 0)
completion_percentage = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
# Show certificate preview
certificate_html = create_completion_certificate(subject, completion_percentage, total_tasks, completed_tasks, total_weeks, completed_weeks)
if certificate_html:
with st.expander("ποΈ View Certificate Preview", expanded=True):
st.components.v1.html(certificate_html, height=800, scrolling=True)
# Show certificate download options only if 90%+ complete
if completion_percentage >= 90:
st.success("π **Congratulations! You've unlocked the certificate download!**")
col1, col2 = st.columns(2)
with col1:
st.download_button(
label="ποΈ Download Certificate (HTML)",
data=certificate_html,
file_name=f"certificate_{subject.replace(' ', '_')}.html",
mime="text/html",
use_container_width=True,
help="Download your certificate as HTML file"
)
with col2:
st.download_button(
label="π Download Certificate (TXT)",
data=f"""CERTIFICATE OF COMPLETION
Subject: {subject}
Completion: {completion_percentage:.1f}%
Tasks Mastered: {completed_tasks}/{total_tasks}
Weeks Completed: {completed_weeks}/{total_weeks}
Date: {datetime.now().strftime("%B %d, %Y")}
This certifies successful completion of the {subject} study program.
Generated by AI Study Planner Pro.""",
file_name=f"certificate_{subject.replace(' ', '_')}.txt",
mime="text/plain",
use_container_width=True,
help="Download certificate details as text file"
)
else:
remaining_percentage = 90 - completion_percentage
st.info(f"π **Complete {remaining_percentage:.1f}% more to unlock certificate download!**")
st.progress(completion_percentage / 100)
st.caption(f"Current progress: {completion_percentage:.1f}% (Minimum 90% required for certificate download)")
else:
st.info("π **Generate or load a study plan first to export!**")