Learnix-AI-Lab commited on
Commit
084f1e7
·
verified ·
1 Parent(s): 4383d42

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +970 -0
  2. requirements.txt +8 -0
app.py ADDED
@@ -0,0 +1,970 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import numpy as np
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from datetime import datetime, date
9
+ import matplotlib.pyplot as plt
10
+
11
+ # =========================================================
12
+ # StudyPilot AI
13
+ # Reinforcement Learning based Smart Study Planner
14
+ # Hugging Face Gradio Deployment Ready
15
+ # =========================================================
16
+
17
+ DATA_DIR = Path("data")
18
+ DATA_DIR.mkdir(exist_ok=True)
19
+
20
+ PROFILE_FILE = DATA_DIR / "profile.json"
21
+ SUBJECTS_FILE = DATA_DIR / "subjects.csv"
22
+ TOPICS_FILE = DATA_DIR / "topics.csv"
23
+ Q_TABLE_FILE = DATA_DIR / "q_table.json"
24
+ LOGS_FILE = DATA_DIR / "study_logs.csv"
25
+
26
+ ACTIONS = [
27
+ "study_new_topic",
28
+ "revise_topic",
29
+ "practice_questions",
30
+ "take_quiz",
31
+ "quick_review"
32
+ ]
33
+
34
+ DEFAULT_PROFILE = {
35
+ "name": "Student",
36
+ "exam_type": "HSC",
37
+ "exam_date": str(date.today()),
38
+ "daily_hours": 3,
39
+ "target_grade": "A+",
40
+ "study_mode": "Balanced"
41
+ }
42
+
43
+
44
+ # =========================================================
45
+ # Storage Helpers
46
+ # =========================================================
47
+
48
+ def ensure_files():
49
+ if not PROFILE_FILE.exists():
50
+ save_json(PROFILE_FILE, DEFAULT_PROFILE)
51
+
52
+ if not SUBJECTS_FILE.exists():
53
+ pd.DataFrame(columns=[
54
+ "subject", "importance", "confidence"
55
+ ]).to_csv(SUBJECTS_FILE, index=False)
56
+
57
+ if not TOPICS_FILE.exists():
58
+ pd.DataFrame(columns=[
59
+ "subject", "topic", "difficulty", "importance", "estimated_hours",
60
+ "confidence", "quiz_score", "completed", "last_studied", "skip_count"
61
+ ]).to_csv(TOPICS_FILE, index=False)
62
+
63
+ if not Q_TABLE_FILE.exists():
64
+ save_json(Q_TABLE_FILE, {})
65
+
66
+ if not LOGS_FILE.exists():
67
+ pd.DataFrame(columns=[
68
+ "date", "subject", "topic", "action", "completed",
69
+ "study_minutes", "old_score", "new_score", "reward"
70
+ ]).to_csv(LOGS_FILE, index=False)
71
+
72
+
73
+ def load_json(path, default=None):
74
+ if not path.exists():
75
+ return default if default is not None else {}
76
+ try:
77
+ with open(path, "r", encoding="utf-8") as f:
78
+ return json.load(f)
79
+ except Exception:
80
+ return default if default is not None else {}
81
+
82
+
83
+ def save_json(path, data):
84
+ with open(path, "w", encoding="utf-8") as f:
85
+ json.dump(data, f, indent=2, ensure_ascii=False)
86
+
87
+
88
+ def load_subjects():
89
+ ensure_files()
90
+ return pd.read_csv(SUBJECTS_FILE)
91
+
92
+
93
+ def save_subjects(df):
94
+ df.to_csv(SUBJECTS_FILE, index=False)
95
+
96
+
97
+ def load_topics():
98
+ ensure_files()
99
+ df = pd.read_csv(TOPICS_FILE)
100
+ if not df.empty:
101
+ df["completed"] = df["completed"].astype(str).map(
102
+ {"True": True, "False": False, "true": True, "false": False, "1": True, "0": False}
103
+ ).fillna(False)
104
+ numeric_cols = ["difficulty", "importance", "estimated_hours", "confidence", "quiz_score", "skip_count"]
105
+ for col in numeric_cols:
106
+ if col in df.columns:
107
+ df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0)
108
+ return df
109
+
110
+
111
+ def save_topics(df):
112
+ df.to_csv(TOPICS_FILE, index=False)
113
+
114
+
115
+ def load_q_table():
116
+ ensure_files()
117
+ return load_json(Q_TABLE_FILE, {})
118
+
119
+
120
+ def save_q_table(q_table):
121
+ save_json(Q_TABLE_FILE, q_table)
122
+
123
+
124
+ def load_logs():
125
+ ensure_files()
126
+ return pd.read_csv(LOGS_FILE)
127
+
128
+
129
+ def save_logs(df):
130
+ df.to_csv(LOGS_FILE, index=False)
131
+
132
+
133
+ ensure_files()
134
+
135
+
136
+ # =========================================================
137
+ # Core Logic
138
+ # =========================================================
139
+
140
+ def safe_float(x, default=0.0):
141
+ try:
142
+ if x is None or x == "":
143
+ return default
144
+ return float(x)
145
+ except Exception:
146
+ return default
147
+
148
+
149
+ def parse_date(date_str):
150
+ try:
151
+ return datetime.strptime(str(date_str), "%Y-%m-%d").date()
152
+ except Exception:
153
+ return date.today()
154
+
155
+
156
+ def get_days_left():
157
+ profile = load_json(PROFILE_FILE, DEFAULT_PROFILE)
158
+ exam_date = parse_date(profile.get("exam_date", str(date.today())))
159
+ return max((exam_date - date.today()).days, 0)
160
+
161
+
162
+ def topic_key(subject, topic):
163
+ return f"{subject}::{topic}"
164
+
165
+
166
+ def get_q_values(subject, topic):
167
+ q_table = load_q_table()
168
+ key = topic_key(subject, topic)
169
+ if key not in q_table:
170
+ q_table[key] = {action: 0.5 for action in ACTIONS}
171
+ save_q_table(q_table)
172
+ return q_table[key]
173
+
174
+
175
+ def best_action(subject, topic, study_mode="Balanced"):
176
+ q_values = get_q_values(subject, topic)
177
+
178
+ if study_mode == "Revision Focused":
179
+ q_values["revise_topic"] += 0.10
180
+ q_values["quick_review"] += 0.08
181
+ elif study_mode == "Weakness Killer":
182
+ q_values["practice_questions"] += 0.12
183
+ q_values["take_quiz"] += 0.08
184
+ elif study_mode == "Exam Crash":
185
+ q_values["revise_topic"] += 0.14
186
+ q_values["practice_questions"] += 0.12
187
+ q_values["quick_review"] += 0.10
188
+ elif study_mode == "Confidence Builder":
189
+ q_values["quick_review"] += 0.08
190
+ q_values["study_new_topic"] += 0.04
191
+
192
+ return max(q_values, key=q_values.get), max(q_values.values())
193
+
194
+
195
+ def days_since_last_studied(last_studied):
196
+ try:
197
+ d = datetime.strptime(str(last_studied), "%Y-%m-%d").date()
198
+ return max((date.today() - d).days, 0)
199
+ except Exception:
200
+ return 30
201
+
202
+
203
+ def calculate_topic_priority(row, study_mode="Balanced"):
204
+ days_left = max(get_days_left(), 1)
205
+
206
+ quiz_score = safe_float(row.get("quiz_score", 0))
207
+ confidence = safe_float(row.get("confidence", 5))
208
+ difficulty = safe_float(row.get("difficulty", 5))
209
+ importance = safe_float(row.get("importance", 5))
210
+ completed = bool(row.get("completed", False))
211
+ skip_count = safe_float(row.get("skip_count", 0))
212
+
213
+ weakness_score = max(0, 100 - quiz_score) / 100
214
+ confidence_weakness = max(0, 10 - confidence) / 10
215
+ revision_need = min(days_since_last_studied(row.get("last_studied", "")) / 14, 1.0)
216
+ exam_pressure = min(30 / days_left, 3.0) / 3.0
217
+ incomplete_bonus = 0 if completed else 1
218
+
219
+ action, q_value = best_action(row["subject"], row["topic"], study_mode)
220
+
221
+ # Mode weights
222
+ if study_mode == "Weakness Killer":
223
+ w_weakness, w_importance, w_revision = 0.38, 0.24, 0.10
224
+ elif study_mode == "Revision Focused":
225
+ w_weakness, w_importance, w_revision = 0.22, 0.24, 0.30
226
+ elif study_mode == "Exam Crash":
227
+ w_weakness, w_importance, w_revision = 0.25, 0.33, 0.22
228
+ elif study_mode == "Confidence Builder":
229
+ w_weakness, w_importance, w_revision = 0.24, 0.22, 0.16
230
+ else:
231
+ w_weakness, w_importance, w_revision = 0.30, 0.27, 0.16
232
+
233
+ score = (
234
+ (importance / 10) * w_importance +
235
+ (difficulty / 10) * 0.10 +
236
+ weakness_score * w_weakness +
237
+ confidence_weakness * 0.10 +
238
+ revision_need * w_revision +
239
+ exam_pressure * 0.10 +
240
+ incomplete_bonus * 0.05 +
241
+ min(skip_count / 5, 1) * 0.04 +
242
+ q_value * 0.08
243
+ )
244
+
245
+ return round(score * 100, 2), action
246
+
247
+
248
+ def explain_reason(row, action):
249
+ reasons = []
250
+ quiz = safe_float(row.get("quiz_score", 0))
251
+ confidence = safe_float(row.get("confidence", 5))
252
+ importance = safe_float(row.get("importance", 5))
253
+ difficulty = safe_float(row.get("difficulty", 5))
254
+ last_days = days_since_last_studied(row.get("last_studied", ""))
255
+
256
+ if quiz < 50:
257
+ reasons.append("quiz score is low")
258
+ if confidence <= 4:
259
+ reasons.append("confidence is low")
260
+ if importance >= 8:
261
+ reasons.append("exam importance is high")
262
+ if difficulty >= 8:
263
+ reasons.append("topic difficulty is high")
264
+ if last_days >= 7:
265
+ reasons.append("revision gap is large")
266
+ if not reasons:
267
+ reasons.append("it is useful for steady progress")
268
+
269
+ action_text = action.replace("_", " ")
270
+ return f"Recommended action: {action_text}. Reason: " + ", ".join(reasons) + "."
271
+
272
+
273
+ def action_to_task(action):
274
+ mapping = {
275
+ "study_new_topic": "Study concept + make short notes",
276
+ "revise_topic": "Revise notes + solve examples",
277
+ "practice_questions": "Practice MCQ/CQ/problem solving",
278
+ "take_quiz": "Take quiz and review mistakes",
279
+ "quick_review": "Quick revision and formula review"
280
+ }
281
+ return mapping.get(action, "Study this topic")
282
+
283
+
284
+ def generate_today_plan(max_topics=3):
285
+ profile = load_json(PROFILE_FILE, DEFAULT_PROFILE)
286
+ study_mode = profile.get("study_mode", "Balanced")
287
+ daily_hours = max(safe_float(profile.get("daily_hours", 3), 3), 1)
288
+ topics = load_topics()
289
+
290
+ if topics.empty:
291
+ return "No topics added yet. Please add subjects and topics first.", pd.DataFrame(), None
292
+
293
+ scored_rows = []
294
+ for _, row in topics.iterrows():
295
+ score, action = calculate_topic_priority(row, study_mode)
296
+ scored_rows.append({
297
+ "Priority Score": score,
298
+ "Subject": row["subject"],
299
+ "Topic": row["topic"],
300
+ "Recommended Task": action_to_task(action),
301
+ "Action": action,
302
+ "Quiz Score": row["quiz_score"],
303
+ "Confidence": row["confidence"],
304
+ "Importance": row["importance"],
305
+ "Difficulty": row["difficulty"],
306
+ "Reason": explain_reason(row, action)
307
+ })
308
+
309
+ plan_df = pd.DataFrame(scored_rows).sort_values("Priority Score", ascending=False).head(int(max_topics))
310
+
311
+ total_priority = plan_df["Priority Score"].sum()
312
+ total_minutes = int(daily_hours * 60)
313
+
314
+ if total_priority <= 0:
315
+ plan_df["Study Time"] = int(total_minutes / len(plan_df))
316
+ else:
317
+ plan_df["Study Time"] = plan_df["Priority Score"].apply(
318
+ lambda x: max(20, int((x / total_priority) * total_minutes))
319
+ )
320
+
321
+ days_left = get_days_left()
322
+ readiness = calculate_readiness()
323
+
324
+ message = f"""
325
+ # Today’s Smart Study Plan
326
+
327
+ **Exam in:** {days_left} days
328
+ **Study Mode:** {study_mode}
329
+ **Daily Study Time:** {daily_hours} hours
330
+ **Overall Readiness:** {readiness}%
331
+
332
+ Start with the highest priority topic first. The app ranks topics using quiz score, confidence, importance, revision gap, exam pressure, and RL memory.
333
+ """
334
+ fig = make_priority_chart(plan_df)
335
+ return message, plan_df, fig
336
+
337
+
338
+ def calculate_readiness():
339
+ topics = load_topics()
340
+ if topics.empty:
341
+ return 0
342
+
343
+ quiz_component = topics["quiz_score"].clip(0, 100).mean()
344
+ confidence_component = (topics["confidence"].clip(0, 10).mean() / 10) * 100
345
+ completion_component = topics["completed"].astype(bool).mean() * 100 if len(topics) else 0
346
+
347
+ readiness = quiz_component * 0.45 + confidence_component * 0.30 + completion_component * 0.25
348
+ return round(float(readiness), 1)
349
+
350
+
351
+ def calculate_risk_label(readiness):
352
+ if readiness >= 80:
353
+ return "Low Risk"
354
+ elif readiness >= 60:
355
+ return "Medium Risk"
356
+ elif readiness >= 40:
357
+ return "High Risk"
358
+ return "Very High Risk"
359
+
360
+
361
+ def make_priority_chart(plan_df):
362
+ if plan_df is None or plan_df.empty:
363
+ return None
364
+ fig, ax = plt.subplots(figsize=(8, 4))
365
+ labels = [f"{s}\n{t}" for s, t in zip(plan_df["Subject"], plan_df["Topic"])]
366
+ ax.bar(labels, plan_df["Priority Score"])
367
+ ax.set_title("Today’s Topic Priority")
368
+ ax.set_ylabel("Priority Score")
369
+ ax.tick_params(axis="x", labelrotation=20)
370
+ plt.tight_layout()
371
+ return fig
372
+
373
+
374
+ def make_subject_progress_chart():
375
+ topics = load_topics()
376
+ if topics.empty:
377
+ return None
378
+
379
+ grouped = topics.groupby("subject").agg(
380
+ avg_quiz=("quiz_score", "mean"),
381
+ avg_confidence=("confidence", "mean"),
382
+ completion=("completed", lambda x: x.astype(bool).mean() * 100)
383
+ ).reset_index()
384
+
385
+ grouped["readiness"] = (
386
+ grouped["avg_quiz"] * 0.50 +
387
+ (grouped["avg_confidence"] / 10 * 100) * 0.25 +
388
+ grouped["completion"] * 0.25
389
+ )
390
+
391
+ fig, ax = plt.subplots(figsize=(8, 4))
392
+ ax.bar(grouped["subject"], grouped["readiness"])
393
+ ax.set_ylim(0, 100)
394
+ ax.set_title("Subject Readiness")
395
+ ax.set_ylabel("Readiness %")
396
+ ax.tick_params(axis="x", labelrotation=20)
397
+ plt.tight_layout()
398
+ return fig
399
+
400
+
401
+ def calculate_reward(old_score, new_score, completed, study_minutes, topic_row):
402
+ reward = 0
403
+ old_score = safe_float(old_score, 0)
404
+ new_score = safe_float(new_score, 0)
405
+ study_minutes = safe_float(study_minutes, 0)
406
+
407
+ importance = safe_float(topic_row.get("importance", 5), 5)
408
+ quiz_improvement = new_score - old_score
409
+
410
+ if completed:
411
+ reward += 20
412
+ else:
413
+ reward -= 10
414
+
415
+ if quiz_improvement > 0:
416
+ reward += min(30, quiz_improvement)
417
+ elif quiz_improvement < 0:
418
+ reward -= min(15, abs(quiz_improvement))
419
+
420
+ if old_score < 50 and completed:
421
+ reward += 15
422
+
423
+ if importance >= 8 and completed:
424
+ reward += 10
425
+
426
+ if study_minutes >= 30:
427
+ reward += 10
428
+
429
+ if not completed and importance >= 8:
430
+ reward -= 15
431
+
432
+ return round(float(reward), 2)
433
+
434
+
435
+ def update_q_table(subject, topic, action, reward):
436
+ q_table = load_q_table()
437
+ key = topic_key(subject, topic)
438
+
439
+ if key not in q_table:
440
+ q_table[key] = {a: 0.5 for a in ACTIONS}
441
+
442
+ if action not in q_table[key]:
443
+ q_table[key][action] = 0.5
444
+
445
+ old_q = q_table[key][action]
446
+ learning_rate = 0.12
447
+ discount_factor = 0.90
448
+ max_future_q = max(q_table[key].values())
449
+
450
+ normalized_reward = reward / 100
451
+ new_q = old_q + learning_rate * (normalized_reward + discount_factor * max_future_q - old_q)
452
+ q_table[key][action] = round(float(new_q), 4)
453
+
454
+ save_q_table(q_table)
455
+ return old_q, new_q
456
+
457
+
458
+ # =========================================================
459
+ # Gradio Functions
460
+ # =========================================================
461
+
462
+ def save_profile(name, exam_type, exam_date, daily_hours, target_grade, study_mode):
463
+ profile = {
464
+ "name": name or "Student",
465
+ "exam_type": exam_type or "General Exam",
466
+ "exam_date": exam_date,
467
+ "daily_hours": safe_float(daily_hours, 3),
468
+ "target_grade": target_grade or "A+",
469
+ "study_mode": study_mode or "Balanced"
470
+ }
471
+ save_json(PROFILE_FILE, profile)
472
+ return f"Profile saved for {profile['name']}."
473
+
474
+
475
+ def get_profile_summary():
476
+ profile = load_json(PROFILE_FILE, DEFAULT_PROFILE)
477
+ days_left = get_days_left()
478
+ readiness = calculate_readiness()
479
+ risk = calculate_risk_label(readiness)
480
+ return f"""
481
+ # StudyPilot AI Dashboard
482
+
483
+ **Student:** {profile.get("name", "Student")}
484
+ **Exam:** {profile.get("exam_type", "Exam")}
485
+ **Exam Date:** {profile.get("exam_date", "")}
486
+ **Days Left:** {days_left}
487
+ **Daily Study Hours:** {profile.get("daily_hours", 3)}
488
+ **Study Mode:** {profile.get("study_mode", "Balanced")}
489
+ **Overall Readiness:** {readiness}%
490
+ **Risk Level:** {risk}
491
+ """
492
+
493
+
494
+ def add_subject(subject, importance, confidence):
495
+ subject = str(subject).strip()
496
+ if not subject:
497
+ return "Subject name is required.", load_subjects()
498
+
499
+ df = load_subjects()
500
+ if not df.empty and subject.lower() in df["subject"].astype(str).str.lower().values:
501
+ return "This subject already exists.", df
502
+
503
+ new_row = pd.DataFrame([{
504
+ "subject": subject,
505
+ "importance": int(safe_float(importance, 5)),
506
+ "confidence": int(safe_float(confidence, 5))
507
+ }])
508
+ df = pd.concat([df, new_row], ignore_index=True)
509
+ save_subjects(df)
510
+ return f"Subject added: {subject}", df
511
+
512
+
513
+ def add_topic(subject, topic, difficulty, importance, estimated_hours, confidence, quiz_score):
514
+ subject = str(subject).strip()
515
+ topic = str(topic).strip()
516
+
517
+ if not subject or not topic:
518
+ return "Subject and topic are required.", load_topics()
519
+
520
+ subjects = load_subjects()
521
+ if subjects.empty or subject.lower() not in subjects["subject"].astype(str).str.lower().values:
522
+ # Auto-create subject for convenience
523
+ subjects = pd.concat([subjects, pd.DataFrame([{
524
+ "subject": subject,
525
+ "importance": 7,
526
+ "confidence": 5
527
+ }])], ignore_index=True)
528
+ save_subjects(subjects)
529
+
530
+ df = load_topics()
531
+ duplicate = False
532
+ if not df.empty:
533
+ duplicate = ((df["subject"].astype(str).str.lower() == subject.lower()) &
534
+ (df["topic"].astype(str).str.lower() == topic.lower())).any()
535
+ if duplicate:
536
+ return "This topic already exists under this subject.", df
537
+
538
+ new_row = pd.DataFrame([{
539
+ "subject": subject,
540
+ "topic": topic,
541
+ "difficulty": int(safe_float(difficulty, 5)),
542
+ "importance": int(safe_float(importance, 5)),
543
+ "estimated_hours": safe_float(estimated_hours, 2),
544
+ "confidence": int(safe_float(confidence, 5)),
545
+ "quiz_score": int(safe_float(quiz_score, 0)),
546
+ "completed": False,
547
+ "last_studied": str(date.today()),
548
+ "skip_count": 0
549
+ }])
550
+ df = pd.concat([df, new_row], ignore_index=True)
551
+ save_topics(df)
552
+
553
+ # Create initial q values
554
+ get_q_values(subject, topic)
555
+
556
+ return f"Topic added: {subject} → {topic}", df
557
+
558
+
559
+ def get_subject_choices():
560
+ df = load_subjects()
561
+ if df.empty:
562
+ return []
563
+ return sorted(df["subject"].dropna().astype(str).unique().tolist())
564
+
565
+
566
+ def get_topic_choices():
567
+ df = load_topics()
568
+ if df.empty:
569
+ return []
570
+ return [f"{row.subject} :: {row.topic}" for row in df.itertuples()]
571
+
572
+
573
+ def refresh_all_tables():
574
+ return load_subjects(), load_topics(), load_logs(), get_profile_summary(), make_subject_progress_chart()
575
+
576
+
577
+ def submit_feedback(topic_selection, action, completed, study_minutes, new_quiz_score, difficulty_after):
578
+ if not topic_selection:
579
+ return "Please select a topic.", load_topics(), load_logs(), make_subject_progress_chart()
580
+
581
+ try:
582
+ subject, topic = [x.strip() for x in topic_selection.split("::", 1)]
583
+ except Exception:
584
+ return "Invalid topic selection.", load_topics(), load_logs(), make_subject_progress_chart()
585
+
586
+ topics = load_topics()
587
+ mask = (topics["subject"].astype(str) == subject) & (topics["topic"].astype(str) == topic)
588
+
589
+ if not mask.any():
590
+ return "Topic not found.", topics, load_logs(), make_subject_progress_chart()
591
+
592
+ idx = topics[mask].index[0]
593
+ old_score = safe_float(topics.loc[idx, "quiz_score"], 0)
594
+ completed_bool = bool(completed)
595
+ new_score = safe_float(new_quiz_score, old_score)
596
+
597
+ topic_row = topics.loc[idx].to_dict()
598
+ reward = calculate_reward(old_score, new_score, completed_bool, study_minutes, topic_row)
599
+
600
+ old_q, new_q = update_q_table(subject, topic, action, reward)
601
+
602
+ topics.loc[idx, "quiz_score"] = new_score
603
+ topics.loc[idx, "completed"] = completed_bool
604
+ topics.loc[idx, "confidence"] = difficulty_to_confidence(difficulty_after, topics.loc[idx, "confidence"])
605
+ topics.loc[idx, "last_studied"] = str(date.today())
606
+
607
+ if completed_bool:
608
+ topics.loc[idx, "skip_count"] = max(0, safe_float(topics.loc[idx, "skip_count"], 0) - 1)
609
+ else:
610
+ topics.loc[idx, "skip_count"] = safe_float(topics.loc[idx, "skip_count"], 0) + 1
611
+
612
+ save_topics(topics)
613
+
614
+ logs = load_logs()
615
+ new_log = pd.DataFrame([{
616
+ "date": str(date.today()),
617
+ "subject": subject,
618
+ "topic": topic,
619
+ "action": action,
620
+ "completed": completed_bool,
621
+ "study_minutes": safe_float(study_minutes, 0),
622
+ "old_score": old_score,
623
+ "new_score": new_score,
624
+ "reward": reward
625
+ }])
626
+ logs = pd.concat([logs, new_log], ignore_index=True)
627
+ save_logs(logs)
628
+
629
+ msg = f"""
630
+ # Feedback Saved
631
+
632
+ **Topic:** {subject} → {topic}
633
+ **Reward:** {reward} points
634
+ **Q-value updated:** {round(old_q, 4)} → {round(new_q, 4)}
635
+
636
+ The planner will use this feedback to improve your next recommendation.
637
+ """
638
+ return msg, topics, logs, make_subject_progress_chart()
639
+
640
+
641
+ def difficulty_to_confidence(difficulty_after, current_confidence):
642
+ current = safe_float(current_confidence, 5)
643
+ if difficulty_after == "Easy":
644
+ return min(10, current + 1)
645
+ if difficulty_after == "Medium":
646
+ return current
647
+ if difficulty_after == "Hard":
648
+ return max(1, current - 1)
649
+ return current
650
+
651
+
652
+ def weak_topics_table():
653
+ topics = load_topics()
654
+ if topics.empty:
655
+ return pd.DataFrame()
656
+
657
+ df = topics.copy()
658
+ df["weakness"] = 100 - df["quiz_score"]
659
+ df = df.sort_values(["weakness", "importance"], ascending=False)
660
+ return df[["subject", "topic", "quiz_score", "confidence", "importance", "difficulty", "completed"]].head(10)
661
+
662
+
663
+ def ai_coach(question):
664
+ profile = load_json(PROFILE_FILE, DEFAULT_PROFILE)
665
+ topics = load_topics()
666
+ readiness = calculate_readiness()
667
+ risk = calculate_risk_label(readiness)
668
+ days_left = get_days_left()
669
+
670
+ if topics.empty:
671
+ return "Add some subjects and topics first. Then I can coach you based on your progress."
672
+
673
+ weakest = topics.sort_values(["quiz_score", "confidence"], ascending=[True, True]).head(1).iloc[0]
674
+ strongest = topics.sort_values(["quiz_score", "confidence"], ascending=[False, False]).head(1).iloc[0]
675
+
676
+ q = (question or "").lower()
677
+
678
+ if "why" in q or "keno" in q or "কেন" in q:
679
+ return f"""
680
+ You should focus on **{weakest['subject']} → {weakest['topic']}** because your quiz score is **{weakest['quiz_score']}%**, confidence is **{weakest['confidence']}/10**, and importance is **{weakest['importance']}/10**.
681
+
682
+ Exam is in **{days_left} days**, so weak and important topics should get priority.
683
+ """
684
+
685
+ if "motivation" in q or "motivate" in q:
686
+ return f"""
687
+ You do not need to finish everything today. Your only goal is to improve one weak topic.
688
+
689
+ Start with **{weakest['topic']}** for 25 minutes. Small progress daily beats panic study before exam.
690
+ """
691
+
692
+ if "risk" in q or "readiness" in q:
693
+ return f"""
694
+ Your current readiness is **{readiness}%** and your risk level is **{risk}**.
695
+
696
+ Weakest topic: **{weakest['subject']} → {weakest['topic']}**
697
+ Strongest topic: **{strongest['subject']} → {strongest['topic']}**
698
+
699
+ Improve weak topics first to reduce exam risk.
700
+ """
701
+
702
+ return f"""
703
+ Based on your current data:
704
+
705
+ - Readiness: **{readiness}%**
706
+ - Risk: **{risk}**
707
+ - Days left: **{days_left}**
708
+ - Weakest topic: **{weakest['subject']} → {weakest['topic']}**
709
+ - Best topic: **{strongest['subject']} → {strongest['topic']}**
710
+
711
+ Recommended next step: study your weakest high-importance topic first, then take a short quiz.
712
+ """
713
+
714
+
715
+ def reset_demo_data():
716
+ save_json(PROFILE_FILE, DEFAULT_PROFILE)
717
+
718
+ subjects = pd.DataFrame([
719
+ {"subject": "Math", "importance": 10, "confidence": 3},
720
+ {"subject": "Physics", "importance": 9, "confidence": 4},
721
+ {"subject": "English", "importance": 7, "confidence": 6}
722
+ ])
723
+ save_subjects(subjects)
724
+
725
+ topics = pd.DataFrame([
726
+ {
727
+ "subject": "Math",
728
+ "topic": "Differentiation",
729
+ "difficulty": 9,
730
+ "importance": 10,
731
+ "estimated_hours": 5,
732
+ "confidence": 3,
733
+ "quiz_score": 35,
734
+ "completed": False,
735
+ "last_studied": str(date.today()),
736
+ "skip_count": 1
737
+ },
738
+ {
739
+ "subject": "Physics",
740
+ "topic": "Current Electricity",
741
+ "difficulty": 8,
742
+ "importance": 9,
743
+ "estimated_hours": 4,
744
+ "confidence": 4,
745
+ "quiz_score": 42,
746
+ "completed": False,
747
+ "last_studied": str(date.today()),
748
+ "skip_count": 0
749
+ },
750
+ {
751
+ "subject": "English",
752
+ "topic": "Writing Part",
753
+ "difficulty": 6,
754
+ "importance": 8,
755
+ "estimated_hours": 3,
756
+ "confidence": 6,
757
+ "quiz_score": 65,
758
+ "completed": False,
759
+ "last_studied": str(date.today()),
760
+ "skip_count": 0
761
+ }
762
+ ])
763
+ save_topics(topics)
764
+
765
+ save_json(Q_TABLE_FILE, {})
766
+ save_logs(pd.DataFrame(columns=[
767
+ "date", "subject", "topic", "action", "completed",
768
+ "study_minutes", "old_score", "new_score", "reward"
769
+ ]))
770
+
771
+ return "Demo data loaded.", subjects, topics, load_logs(), get_profile_summary(), make_subject_progress_chart()
772
+
773
+
774
+ def clear_all_data():
775
+ save_json(PROFILE_FILE, DEFAULT_PROFILE)
776
+ save_subjects(pd.DataFrame(columns=["subject", "importance", "confidence"]))
777
+ save_topics(pd.DataFrame(columns=[
778
+ "subject", "topic", "difficulty", "importance", "estimated_hours",
779
+ "confidence", "quiz_score", "completed", "last_studied", "skip_count"
780
+ ]))
781
+ save_json(Q_TABLE_FILE, {})
782
+ save_logs(pd.DataFrame(columns=[
783
+ "date", "subject", "topic", "action", "completed",
784
+ "study_minutes", "old_score", "new_score", "reward"
785
+ ]))
786
+ return "All data cleared.", load_subjects(), load_topics(), load_logs(), get_profile_summary(), None
787
+
788
+
789
+ # =========================================================
790
+ # Gradio UI
791
+ # =========================================================
792
+
793
+ custom_css = """
794
+ #main-title {
795
+ text-align: center;
796
+ margin-bottom: 10px;
797
+ }
798
+ .metric-card {
799
+ border-radius: 14px;
800
+ padding: 16px;
801
+ }
802
+ """
803
+
804
+ with gr.Blocks(
805
+ theme=gr.themes.Soft(),
806
+ css=custom_css,
807
+ title="StudyPilot AI"
808
+ ) as demo:
809
+
810
+ gr.Markdown(
811
+ """
812
+ # StudyPilot AI
813
+ ### Reinforcement Learning Based Smart Study Planner
814
+
815
+ Dynamic subjects, adaptive topic recommendation, quiz feedback, reward system, and progress analytics.
816
+ """,
817
+ elem_id="main-title"
818
+ )
819
+
820
+ with gr.Tab("Dashboard"):
821
+ dashboard_md = gr.Markdown(get_profile_summary())
822
+ refresh_btn = gr.Button("Refresh Dashboard")
823
+ subject_chart = gr.Plot(value=make_subject_progress_chart())
824
+ weak_table = gr.Dataframe(value=weak_topics_table(), label="Top Weak Topics", interactive=False)
825
+
826
+ refresh_btn.click(
827
+ fn=lambda: (get_profile_summary(), make_subject_progress_chart(), weak_topics_table()),
828
+ outputs=[dashboard_md, subject_chart, weak_table]
829
+ )
830
+
831
+ with gr.Tab("Student Profile"):
832
+ with gr.Row():
833
+ name = gr.Textbox(label="Student Name", value=load_json(PROFILE_FILE, DEFAULT_PROFILE).get("name", "Student"))
834
+ exam_type = gr.Textbox(label="Exam Type", value=load_json(PROFILE_FILE, DEFAULT_PROFILE).get("exam_type", "HSC"))
835
+ with gr.Row():
836
+ exam_date = gr.Textbox(label="Exam Date YYYY-MM-DD", value=load_json(PROFILE_FILE, DEFAULT_PROFILE).get("exam_date", str(date.today())))
837
+ daily_hours = gr.Number(label="Daily Study Hours", value=load_json(PROFILE_FILE, DEFAULT_PROFILE).get("daily_hours", 3))
838
+ with gr.Row():
839
+ target_grade = gr.Textbox(label="Target Grade", value=load_json(PROFILE_FILE, DEFAULT_PROFILE).get("target_grade", "A+"))
840
+ study_mode = gr.Dropdown(
841
+ choices=["Balanced", "Weakness Killer", "Revision Focused", "Exam Crash", "Confidence Builder"],
842
+ value=load_json(PROFILE_FILE, DEFAULT_PROFILE).get("study_mode", "Balanced"),
843
+ label="Study Mode"
844
+ )
845
+ save_profile_btn = gr.Button("Save Profile")
846
+ profile_status = gr.Markdown()
847
+
848
+ save_profile_btn.click(
849
+ fn=save_profile,
850
+ inputs=[name, exam_type, exam_date, daily_hours, target_grade, study_mode],
851
+ outputs=profile_status
852
+ )
853
+
854
+ with gr.Tab("Subject Manager"):
855
+ gr.Markdown("### Add any subject. Example: Physics, Accounting, Marketing, Programming, English, Biology.")
856
+ with gr.Row():
857
+ subject_input = gr.Textbox(label="Subject Name")
858
+ subject_importance = gr.Slider(1, 10, value=7, step=1, label="Subject Importance")
859
+ subject_confidence = gr.Slider(1, 10, value=5, step=1, label="Current Confidence")
860
+ add_subject_btn = gr.Button("Add Subject")
861
+ subject_status = gr.Markdown()
862
+ subject_table = gr.Dataframe(value=load_subjects(), label="Subjects", interactive=False)
863
+
864
+ add_subject_btn.click(
865
+ fn=add_subject,
866
+ inputs=[subject_input, subject_importance, subject_confidence],
867
+ outputs=[subject_status, subject_table]
868
+ )
869
+
870
+ with gr.Tab("Topic Manager"):
871
+ gr.Markdown("### Add topics under any subject.")
872
+ with gr.Row():
873
+ topic_subject = gr.Textbox(label="Subject Name")
874
+ topic_name = gr.Textbox(label="Topic Name")
875
+ with gr.Row():
876
+ topic_difficulty = gr.Slider(1, 10, value=5, step=1, label="Topic Difficulty")
877
+ topic_importance = gr.Slider(1, 10, value=7, step=1, label="Exam Importance")
878
+ estimated_hours = gr.Number(label="Estimated Hours Needed", value=2)
879
+ with gr.Row():
880
+ topic_confidence = gr.Slider(1, 10, value=5, step=1, label="Your Confidence")
881
+ quiz_score = gr.Slider(0, 100, value=0, step=1, label="Current Quiz Score")
882
+ add_topic_btn = gr.Button("Add Topic")
883
+ topic_status = gr.Markdown()
884
+ topic_table = gr.Dataframe(value=load_topics(), label="Topics", interactive=False)
885
+
886
+ add_topic_btn.click(
887
+ fn=add_topic,
888
+ inputs=[
889
+ topic_subject, topic_name, topic_difficulty, topic_importance,
890
+ estimated_hours, topic_confidence, quiz_score
891
+ ],
892
+ outputs=[topic_status, topic_table]
893
+ )
894
+
895
+ with gr.Tab("Smart Study Plan"):
896
+ gr.Markdown("### Generate today’s adaptive study plan.")
897
+ max_topics = gr.Slider(1, 6, value=3, step=1, label="Number of Topics for Today")
898
+ generate_btn = gr.Button("Generate Today’s Plan")
899
+ plan_md = gr.Markdown()
900
+ plan_table = gr.Dataframe(label="Recommended Plan", interactive=False)
901
+ priority_plot = gr.Plot()
902
+
903
+ generate_btn.click(
904
+ fn=generate_today_plan,
905
+ inputs=max_topics,
906
+ outputs=[plan_md, plan_table, priority_plot]
907
+ )
908
+
909
+ with gr.Tab("Quiz & Feedback"):
910
+ gr.Markdown("### After studying, give feedback. This updates the RL memory.")
911
+ topic_dropdown = gr.Dropdown(choices=get_topic_choices(), label="Select Topic")
912
+ refresh_topics_btn = gr.Button("Refresh Topic List")
913
+ action_dropdown = gr.Dropdown(choices=ACTIONS, value="practice_questions", label="Action Taken")
914
+ completed_checkbox = gr.Checkbox(label="Completed?", value=True)
915
+ study_minutes = gr.Number(label="Study Minutes", value=60)
916
+ new_score = gr.Slider(0, 100, value=50, step=1, label="New Quiz Score")
917
+ difficulty_after = gr.Dropdown(choices=["Easy", "Medium", "Hard"], value="Medium", label="How did the topic feel?")
918
+ feedback_btn = gr.Button("Submit Feedback")
919
+ feedback_output = gr.Markdown()
920
+ updated_topic_table = gr.Dataframe(value=load_topics(), label="Updated Topics", interactive=False)
921
+ log_table = gr.Dataframe(value=load_logs(), label="Study Logs", interactive=False)
922
+ feedback_chart = gr.Plot(value=make_subject_progress_chart())
923
+
924
+ refresh_topics_btn.click(
925
+ fn=lambda: gr.update(choices=get_topic_choices()),
926
+ outputs=topic_dropdown
927
+ )
928
+
929
+ feedback_btn.click(
930
+ fn=submit_feedback,
931
+ inputs=[topic_dropdown, action_dropdown, completed_checkbox, study_minutes, new_score, difficulty_after],
932
+ outputs=[feedback_output, updated_topic_table, log_table, feedback_chart]
933
+ )
934
+
935
+ with gr.Tab("AI Coach"):
936
+ gr.Markdown("### Ask the study coach. Example: Why should I study Math today?")
937
+ coach_question = gr.Textbox(label="Your Question", placeholder="Ask about weakness, risk, motivation, or next topic...")
938
+ coach_btn = gr.Button("Ask Coach")
939
+ coach_answer = gr.Markdown()
940
+
941
+ coach_btn.click(
942
+ fn=ai_coach,
943
+ inputs=coach_question,
944
+ outputs=coach_answer
945
+ )
946
+
947
+ with gr.Tab("Admin / Demo"):
948
+ gr.Markdown("Use demo data for testing on Hugging Face.")
949
+ demo_btn = gr.Button("Load Demo Data")
950
+ clear_btn = gr.Button("Clear All Data")
951
+ admin_status = gr.Markdown()
952
+ admin_subjects = gr.Dataframe(value=load_subjects(), label="Subjects")
953
+ admin_topics = gr.Dataframe(value=load_topics(), label="Topics")
954
+ admin_logs = gr.Dataframe(value=load_logs(), label="Logs")
955
+ admin_dashboard = gr.Markdown(get_profile_summary())
956
+ admin_chart = gr.Plot(value=make_subject_progress_chart())
957
+
958
+ demo_btn.click(
959
+ fn=reset_demo_data,
960
+ outputs=[admin_status, admin_subjects, admin_topics, admin_logs, admin_dashboard, admin_chart]
961
+ )
962
+
963
+ clear_btn.click(
964
+ fn=clear_all_data,
965
+ outputs=[admin_status, admin_subjects, admin_topics, admin_logs, admin_dashboard, admin_chart]
966
+ )
967
+
968
+
969
+ if __name__ == "__main__":
970
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.44.0
2
+ pandas>=2.0.0
3
+ numpy>=1.24.0
4
+ matplotlib>=3.7.0
5
+ scikit-learn
6
+ reportlab
7
+ gtts
8
+ Pillow