StudyPlanner.v2 / app.py
syuanteng315's picture
Update app.py
ca7dce1 verified
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="<p>Attached is your updated personalised study plan!</p>",
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"""<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500&display=swap');
*{{box-sizing:border-box;margin:0;padding:0}}
body{{background:#fdf8f3;display:flex;flex-direction:column;align-items:center;padding:20px;min-height:100vh}}
canvas{{width:100%;max-width:340px;border-radius:20px}}
.btn{{margin-top:16px;width:100%;max-width:340px;padding:13px;background:#1a1a1a;color:#fff;border:none;font-family:'DM Sans',sans-serif;font-size:14px;border-radius:10px;cursor:pointer}}
</style>
</head>
<body>
<canvas id="c" width="720" height="900"></canvas>
<button class="btn" onclick="dl()">Download to share</button>
<script>
const NAME="{name}";
const MINS={mins};
function drawStar(x,cx,cy,r,color){{
x.fillStyle=color;x.beginPath();
for(let i=0;i<10;i++){{
const a=Math.PI/5*i-Math.PI/2;
const rad=i%2===0?r:r*0.4;
const px=cx+Math.cos(a)*rad,py=cy+Math.sin(a)*rad;
i===0?x.moveTo(px,py):x.lineTo(px,py);
}}
x.closePath();x.fill();
}}
function gen(){{
const stars = Math.floor(MINS / 1);
const hrs=Math.floor(MINS/60),rm=MINS%60;
const timeStr=hrs>0?`${{hrs}}h ${{rm}}m`:`${{MINS}}m`;
const W=720,H=900;
const c=document.getElementById('c');
const x=c.getContext('2d');
x.clearRect(0,0,W,H);
const BG='#fdf8f3',INK='#1a1a1a',MUTED='#888',SAGE='#7a9e7e',CREAM='#f5ede0',GOLD='#c9a84c';
x.fillStyle=BG;x.roundRect(0,0,W,H,32);x.fill();
x.fillStyle=CREAM;x.roundRect(0,0,W,120,0);x.fill();
x.fillStyle=CREAM;x.fillRect(0,88,W,32);
x.fillStyle=SAGE;x.fillRect(0,118,W,4);
x.font="500 22px 'DM Sans',sans-serif";x.fillStyle=MUTED;x.textAlign='center';x.letterSpacing='6px';
x.fillText('STUDY WRAPPED',W/2,58);x.letterSpacing='0px';
x.font="300 15px 'DM Sans',sans-serif";x.fillStyle=MUTED;x.fillText('your weekly achievement',W/2,90);
x.font="italic 72px 'DM Serif Display',serif";x.fillStyle=INK;
const dn=NAME.length>12?NAME.substring(0,12)+'…':NAME;
x.fillText(dn,W/2,230);
x.strokeStyle=SAGE;x.lineWidth=1.5;x.beginPath();x.moveTo(100,255);x.lineTo(W-100,255);x.stroke();
x.fillStyle=CREAM;x.roundRect(60,280,600,160,20);x.fill();
x.font="400 13px 'DM Sans',sans-serif";x.fillStyle=MUTED;x.letterSpacing='3px';
x.fillText('TOTAL STUDY TIME',W/2,320);x.letterSpacing='0px';
x.font="300 64px 'DM Serif Display',serif";x.fillStyle=INK;x.fillText(timeStr,W/2,400);
x.strokeStyle='#e0d8ce';x.lineWidth=1;x.beginPath();x.moveTo(60,460);x.lineTo(W-60,460);x.stroke();
x.font="400 13px 'DM Sans',sans-serif";x.fillStyle=MUTED;x.letterSpacing='3px';
x.fillText('STARS EARNED',W/2,500);x.letterSpacing='0px';
x.font="300 80px 'DM Serif Display',serif";x.fillStyle=GOLD;x.fillText(String(stars),W/2,590);
const maxD=7,filled=Math.min(Math.floor(MINS/60),maxD),gap=72,sx=W/2-(maxD-1)*gap/2;
for(let i=0;i<maxD;i++)drawStar(x,sx+i*gap,660,20,i<filled?GOLD:'#e8e0d8');
x.font="300 13px 'DM Sans',sans-serif";x.fillStyle=MUTED;
x.fillText(`${{filled}} of ${{maxD}} hours achieved this week`,W/2,710);
const pct=Math.min(MINS/700,1);
x.fillStyle='#ede5da';x.roundRect(60,740,600,8,4);x.fill();
if(pct>0){{x.fillStyle=SAGE;x.roundRect(60,740,Math.floor(600*pct),8,4);x.fill();}}
x.font="300 12px 'DM Sans',sans-serif";x.fillStyle=MUTED;
x.textAlign='left';x.fillText('0 mins',60,775);
x.textAlign='right';x.fillText('700 mins',W-60,775);
x.strokeStyle='#e0d8ce';x.lineWidth=1;x.beginPath();x.moveTo(60,810);x.lineTo(W-60,810);x.stroke();
x.font="italic 18px 'DM Serif Display',serif";x.fillStyle=MUTED;x.textAlign='center';
x.fillText('keep showing up. it adds up.',W/2,850);
x.font="300 11px 'DM Sans',sans-serif";x.fillStyle='#bbb';x.letterSpacing='2px';
x.fillText('AI STUDY PLANNER',W/2,885);x.letterSpacing='0px';
}}
function dl(){{
const a=document.createElement('a');
a.download='study-card.png';
a.href=document.getElementById('c').toDataURL('image/png');
a.click();
}}
document.fonts.ready.then(()=>gen());
</script>
</body>
</html>"""
return HTMLResponse(content=html)