from fastapi import FastAPI from pydantic import BaseModel import pandas as pd from fpdf import FPDF import sib_api_v3_sdk import base64 import json import os app = FastAPI() class PlanInput(BaseModel): student_email: str subjects: list scores: list feedback: list = [] class FeedbackInput(BaseModel): student_email: str subjects: list scores: list reflections: list class AdaptiveAI: def __init__(self, subjects, scores_list): self.df = pd.DataFrame({ "Subject": subjects, "Score": [int(s) for s in scores_list] }) self.days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] def generate_plan(self, feedback_data=None): def get_base_settings(score): if score < 40: return 150, "Focus on fundamental concepts." if score < 65: return 120, "Practice more topical questions." if score < 80: return 90, "Review mistakes & past year papers." return 60, "Quick revision & advance topics." self.df['Time_Mins'], self.df['Advice'] = zip(*self.df['Score'].apply(get_base_settings)) self.df = self.df.sort_values(by='Score').reset_index(drop=True) self.df['Day'] = self.days[:len(self.df)] if feedback_data: for fb in feedback_data: sub = fb.get('Subject') idx = self.df[self.df['Subject'] == sub].index if not idx.empty: current_t = self.df.loc[idx, 'Time_Mins'].values[0] understanding = fb.get('understanding', 3) focus = fb.get('focus', 3) avg = (understanding + focus) / 2 if avg <= 2: self.df.loc[idx, 'Time_Mins'] = current_t + 30 elif avg >= 4: self.df.loc[idx, 'Time_Mins'] = max(45, current_t - 15) return self.df def generate_ai_comments(reflections): comments = [] for r in reflections: sub = r.get('Subject', '') understanding = r.get('understanding', 3) focus = r.get('focus', 3) study_mins = r.get('study_mins', 0) notes = r.get('notes', '') if understanding >= 4 and focus >= 4: comment = f"You've shown strong understanding and great focus in {sub} throughout this week. Excellent consistency — keep maintaining this level!" elif understanding >= 4 and focus < 3: comment = f"Your grasp of {sub} has been impressive this week. Work on minimising distractions in your next sessions to unlock your full potential." elif focus >= 4 and understanding < 3: comment = f"Your dedication to {sub} this week is clear — you've been putting in the effort. Spend a bit more time revisiting the concepts that felt unclear." elif understanding < 3 and focus < 3: comment = f"{sub} has been a challenge this week, but that's okay. Try breaking your sessions into smaller chunks and find a quieter study space to help you concentrate." else: comment = f"A steady week for {sub}. You're building your foundation — consistency over time will make a big difference." if study_mins >= 90: comment += " Your study duration this week has been commendable." if notes and len(notes) > 10: comment += " Taking notes regularly is a great habit that will pay off during revision." comments.append({"Subject": sub, "comment": comment}) return comments def generate_overall_comment(reflections): if not reflections: return "Stay consistent and keep pushing forward!" avg_understanding = sum(r.get('understanding', 3) for r in reflections) / len(reflections) avg_focus = sum(r.get('focus', 3) for r in reflections) / len(reflections) total_study = sum(r.get('study_mins', 0) for r in reflections) total_break = sum(r.get('break_mins', 0) for r in reflections) if avg_understanding >= 4 and avg_focus >= 4: overall = "What a fantastic week! Your focus and understanding across all subjects have been outstanding. You're clearly putting in real, quality effort — and it shows." elif avg_understanding >= 3.5 or avg_focus >= 3.5: overall = "Great week overall! You've made solid progress across your subjects. Keep building on this momentum and the results will follow." elif avg_understanding < 2.5 and avg_focus < 2.5: overall = "It was a tough week, but the fact that you pushed through and completed your sessions speaks volumes. Rest well, reflect on what held you back, and come back stronger." else: overall = "A decent week of studying! Every session you complete is a step forward. Stay patient with yourself and keep showing up." hours = total_study // 60 mins = total_study % 60 overall += f"\n\nThis week's stats — Total study time: {hours}h {mins}m | Total break time: {total_break} mins." return overall def create_pdf(df, reflections=None): pdf = FPDF() pdf.add_page() pdf.set_left_margin(15) pdf.set_right_margin(15) # ── Header ── pdf.set_fill_color(30, 30, 30) pdf.rect(0, 0, 210, 42, 'F') pdf.set_y(11) pdf.set_text_color(255, 255, 255) pdf.set_font("Arial", 'B', 20) pdf.cell(0, 11, "AI ADAPTIVE STUDY PLAN", ln=True, align='C') pdf.set_font("Arial", '', 10) pdf.set_text_color(180, 180, 180) pdf.cell(0, 7, "Personalised | AI-Generated | Weekly Schedule", ln=True, align='C') pdf.ln(10) # ── Divider ── pdf.set_draw_color(220, 220, 220) pdf.set_line_width(0.3) pdf.line(15, pdf.get_y(), 195, pdf.get_y()) pdf.ln(5) # ── Column headers ── pdf.set_fill_color(245, 245, 245) pdf.set_text_color(80, 80, 80) pdf.set_font("Arial", 'B', 11) pdf.set_draw_color(220, 220, 220) pdf.cell(28, 11, "DAY", border='B', align='C', fill=True) pdf.cell(48, 11, "SUBJECT", border='B', align='C', fill=True) pdf.cell(25, 11, "TIME", border='B', align='C', fill=True) pdf.cell(84, 11, "FOCUS FOR THIS WEEK", border='B', align='L', fill=True) pdf.ln() # ── Table rows ── row_colors = [(255, 255, 255), (250, 250, 250)] accent_colors = [ (99, 179, 237), (104, 211, 145), (246, 173, 85), (252, 129, 74), (154, 117, 234), (237, 100, 166), (72, 187, 120), ] for i, row in df.iterrows(): r, g, b = row_colors[i % 2] pdf.set_fill_color(r, g, b) pdf.set_text_color(60, 60, 60) pdf.set_font("Arial", size=11) ar, ag, ab = accent_colors[i % len(accent_colors)] pdf.set_fill_color(ar, ag, ab) pdf.cell(3, 12, "", fill=True) pdf.set_fill_color(r, g, b) pdf.cell(25, 12, str(row['Day']), align='C', fill=True) pdf.set_font("Arial", 'B', 11) pdf.cell(48, 12, str(row['Subject']), align='C', fill=True) pdf.set_font("Arial", size=11) pdf.set_text_color(ar, ag, ab) pdf.cell(25, 12, f"{row['Time_Mins']} mins", align='C', fill=True) pdf.set_text_color(80, 80, 80) pdf.cell(84, 12, str(row['Advice']), align='L', fill=True) pdf.ln() pdf.ln(6) pdf.set_draw_color(220, 220, 220) pdf.line(15, pdf.get_y(), 195, pdf.get_y()) pdf.ln(6) # ── AI Feedback section ── if reflections: pdf.set_font("Arial", 'B', 12) pdf.set_text_color(30, 30, 30) pdf.cell(0, 9, "Weekly AI Feedback", ln=True) pdf.set_font("Arial", '', 9) pdf.set_text_color(140, 140, 140) pdf.cell(0, 5, "Based on your self-reflection and study sessions this week", ln=True) pdf.ln(4) overall = generate_overall_comment(reflections) pdf.set_fill_color(240, 249, 255) pdf.set_draw_color(147, 210, 255) pdf.set_line_width(0.4) pdf.set_font("Arial", 'B', 10) pdf.set_text_color(30, 100, 160) pdf.cell(0, 8, " Overall Summary", border='LTR', fill=True, ln=True) pdf.set_font("Arial", '', 10) pdf.set_text_color(50, 50, 50) pdf.multi_cell(0, 7, " " + overall, border='LBR', fill=True) pdf.ln(5) subject_comments = generate_ai_comments(reflections) fill_colors = [ (240, 253, 244), (255, 247, 237), (250, 240, 255), (255, 244, 248), (237, 247, 255), (255, 253, 235), (240, 255, 250) ] border_colors = [ (134, 214, 154), (251, 191, 112), (196, 160, 244), (246, 135, 179), (129, 196, 255), (250, 220, 100), (110, 220, 180) ] text_colors = [ (21, 128, 61), (154, 72, 6), (107, 33, 168), (157, 23, 77), (29, 78, 216), (133, 100, 4), (6, 120, 87) ] for idx, sc in enumerate(subject_comments): ci = idx % len(fill_colors) fr, fg, fb2 = fill_colors[ci] br, bg, bb = border_colors[ci] tr, tg, tb = text_colors[ci] pdf.set_fill_color(fr, fg, fb2) pdf.set_draw_color(br, bg, bb) pdf.set_line_width(0.4) pdf.set_font("Arial", 'B', 10) pdf.set_text_color(tr, tg, tb) pdf.cell(0, 8, f" {sc['Subject']}", border='LTR', fill=True, ln=True) pdf.set_font("Arial", '', 10) pdf.set_text_color(60, 60, 60) pdf.multi_cell(0, 7, f" {sc['comment']}", border='LBR', fill=True) pdf.ln(3) # ── Footer (no blank page) ── if pdf.get_y() < 260: pdf.set_y(-18) else: pdf.ln(5) pdf.set_draw_color(220, 220, 220) pdf.line(15, pdf.get_y(), 195, pdf.get_y()) pdf.set_font("Arial", 'I', 8) pdf.set_text_color(180, 180, 180) pdf.cell(0, 8, "Generated by AI Tutor | Keep learning, keep growing.", align='C') pdf_path = "study_plan.pdf" pdf.output(pdf_path) return pdf_path def send_email(email_to, pdf_path, subject="Your New Study Plan"): api_key = "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" configuration = sib_api_v3_sdk.Configuration() configuration.api_key['api-key'] = api_key api_instance = sib_api_v3_sdk.TransactionalEmailsApi(sib_api_v3_sdk.ApiClient(configuration)) with open(pdf_path, "rb") as f: pdf_b64 = base64.b64encode(f.read()).decode('utf-8') smtp_email = sib_api_v3_sdk.SendSmtpEmail( to=[{"email": email_to}], sender={"name": "AI Tutor", "email": "syuantengzhiya@gmail.com"}, subject=subject, html_content="
Attached is your updated personalised study plan!
", attachment=[{"content": pdf_b64, "name": "StudyPlan.pdf"}] ) api_instance.send_transac_email(smtp_email) @app.get("/") def home(): return {"message": "Study Planner API is running!"} @app.post("/plan") def generate_plan(data: PlanInput): try: engine = AdaptiveAI(data.subjects, data.scores) plan_df = engine.generate_plan() pdf_file = create_pdf(plan_df) send_email(data.student_email, pdf_file, subject="Your Study Plan is Ready!") result = plan_df[['Day', 'Subject', 'Time_Mins', 'Advice']].to_dict('records') return {"status": "success", "plan": result} except Exception as e: return {"status": "error", "message": str(e)} @app.post("/update_plan") def update_plan(data: FeedbackInput): try: engine = AdaptiveAI(data.subjects, data.scores) plan_df = engine.generate_plan(feedback_data=data.reflections) pdf_file = create_pdf(plan_df, reflections=data.reflections) send_email(data.student_email, pdf_file, subject="Your Updated Study Plan + Weekly AI Feedback!") result = plan_df[['Day', 'Subject', 'Time_Mins', 'Advice']].to_dict('records') return {"status": "success", "plan": result} except Exception as e: return {"status": "error", "message": str(e)} from fastapi.responses import HTMLResponse @app.get("/card") def study_card(name: str = "Study Hero", mins: int = 0): html = f""" """ return HTMLResponse(content=html)