Spaces:
Sleeping
Sleeping
| 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) | |
| def home(): | |
| return {"message": "Study Planner API is running!"} | |
| 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)} | |
| 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 | |
| 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) |