Kiki0203 commited on
Commit
f9766bc
Β·
verified Β·
1 Parent(s): 9af8807

Upload 13 files

Browse files
Files changed (13) hide show
  1. .gitignore +13 -0
  2. app.py +1243 -0
  3. content_generator.py +63 -0
  4. evaluation.py +53 -0
  5. flashcard_generator.py +36 -0
  6. gamification.py +180 -0
  7. learning_progress.json +46 -0
  8. notes.json +12 -0
  9. pdf_export.py +274 -0
  10. quiz_generator.py +60 -0
  11. requirements.txt +5 -0
  12. tutor.py +43 -0
  13. utils.py +158 -0
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pyo
4
+ venv/
5
+ .env
6
+ .venv/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .DS_Store
11
+ *.log
12
+ learning_progress.json
13
+ notes.json
app.py ADDED
@@ -0,0 +1,1243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import streamlit as st
3
+ import pandas as pd
4
+ from content_generator import generate_content
5
+ from quiz_generator import generate_quiz
6
+ from evaluation import evaluate_answers
7
+ from flashcard_generator import generate_flashcards
8
+ from tutor import get_tutor_reply
9
+ from pdf_export import export_study_notes_pdf, export_quiz_results_pdf
10
+ from gamification import (
11
+ load_gamification, save_gamification,
12
+ update_streak, award_xp, check_and_award_badges,
13
+ get_level, get_xp_for_quiz, record_quiz,
14
+ BADGES, XP_STUDY_SESSION, XP_FLASHCARD_DECK,
15
+ )
16
+ from utils import (
17
+ get_topics, save_progress, load_progress,
18
+ get_weak_topics, get_learning_path,
19
+ load_notes, save_note, delete_note,
20
+ )
21
+
22
+ # ── Page config ──────────────────────────────────────────────────────────────
23
+ st.set_page_config(
24
+ page_title="LearnCraft – Personalized Learning",
25
+ page_icon="πŸŽ“",
26
+ layout="wide",
27
+ initial_sidebar_state="expanded",
28
+ )
29
+
30
+ # ── Light Theme CSS ──────────────────────────────────────────────────────────
31
+ st.markdown("""
32
+ <style>
33
+ @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,400;0,700;0,900;1,400&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap');
34
+
35
+ :root {
36
+ --bg: #f7f4ef;
37
+ --surface: #ffffff;
38
+ --sidebar: #1e1b4b;
39
+ --accent: #6c47ff;
40
+ --accent2: #f97316;
41
+ --accent3: #10b981;
42
+ --text: #1a1523;
43
+ --muted: #6b6880;
44
+ --card: #ffffff;
45
+ --border: #e2ddf5;
46
+ --tag-bg: #ede9fe;
47
+ --tag-color: #6c47ff;
48
+ --shadow: 0 2px 16px rgba(108,71,255,0.08);
49
+ --shadow-lg: 0 8px 40px rgba(108,71,255,0.13);
50
+ }
51
+
52
+ html, body, [class*="css"] {
53
+ font-family: 'Plus Jakarta Sans', sans-serif !important;
54
+ background-color: var(--bg) !important;
55
+ color: var(--text) !important;
56
+ }
57
+
58
+ .stApp { background: var(--bg) !important; }
59
+
60
+ /* Sidebar */
61
+ [data-testid="stSidebar"] {
62
+ background: var(--sidebar) !important;
63
+ border-right: none !important;
64
+ }
65
+ [data-testid="stSidebar"] * { color: #e2e0f5 !important; }
66
+ [data-testid="stSidebar"] h1,
67
+ [data-testid="stSidebar"] h2,
68
+ [data-testid="stSidebar"] h3 { color: #fff !important; }
69
+
70
+ /* Headings */
71
+ h1, h2, h3 {
72
+ font-family: 'Fraunces', serif !important;
73
+ color: var(--text) !important;
74
+ letter-spacing: -0.02em;
75
+ }
76
+
77
+ /* Cards */
78
+ .learn-card {
79
+ background: var(--card);
80
+ border: 1px solid var(--border);
81
+ border-radius: 18px;
82
+ padding: 1.6rem;
83
+ margin: 0.75rem 0;
84
+ box-shadow: var(--shadow);
85
+ transition: transform 0.18s, box-shadow 0.18s;
86
+ }
87
+ .learn-card:hover {
88
+ transform: translateY(-3px);
89
+ box-shadow: var(--shadow-lg);
90
+ border-color: var(--accent);
91
+ }
92
+
93
+ /* Hero */
94
+ .hero {
95
+ background: linear-gradient(135deg, #6c47ff 0%, #a78bfa 55%, #f97316 100%);
96
+ border-radius: 24px;
97
+ padding: 3.5rem 2.5rem;
98
+ text-align: center;
99
+ margin-bottom: 2rem;
100
+ position: relative;
101
+ overflow: hidden;
102
+ box-shadow: var(--shadow-lg);
103
+ }
104
+ .hero::before {
105
+ content: '';
106
+ position: absolute; top:-60%; left:-30%;
107
+ width: 200%; height: 200%;
108
+ background: radial-gradient(circle, rgba(255,255,255,0.12) 0%, transparent 55%);
109
+ pointer-events: none;
110
+ }
111
+ .hero h1 {
112
+ font-family: 'Fraunces', serif !important;
113
+ font-size: 3.2rem !important;
114
+ color: #fff !important;
115
+ margin-bottom: 0.4rem !important;
116
+ text-shadow: 0 2px 12px rgba(0,0,0,0.15);
117
+ }
118
+ .hero p { color: rgba(255,255,255,0.88) !important; font-size: 1.1rem; font-weight: 400; }
119
+
120
+ /* Tag pill */
121
+ .tag {
122
+ display: inline-block;
123
+ background: rgba(255,255,255,0.22);
124
+ color: #fff !important;
125
+ border: 1px solid rgba(255,255,255,0.35);
126
+ border-radius: 100px;
127
+ padding: 0.28rem 0.9rem;
128
+ font-size: 0.75rem;
129
+ font-weight: 700;
130
+ letter-spacing: 0.07em;
131
+ text-transform: uppercase;
132
+ margin-bottom: 1rem;
133
+ }
134
+
135
+ /* Content blocks */
136
+ .content-block {
137
+ background: var(--card);
138
+ border-left: 4px solid var(--accent);
139
+ border-radius: 0 14px 14px 0;
140
+ padding: 1.5rem 2rem;
141
+ margin: 0.9rem 0;
142
+ box-shadow: var(--shadow);
143
+ line-height: 1.8;
144
+ }
145
+ .content-block h3 {
146
+ color: var(--accent) !important;
147
+ font-size: 1.15rem !important;
148
+ margin-bottom: 0.6rem !important;
149
+ }
150
+
151
+ /* Score badge */
152
+ .score-badge {
153
+ font-size: 4.5rem;
154
+ font-family: 'Fraunces', serif;
155
+ font-weight: 900;
156
+ background: linear-gradient(135deg, #6c47ff, #f97316);
157
+ -webkit-background-clip: text;
158
+ -webkit-text-fill-color: transparent;
159
+ background-clip: text;
160
+ }
161
+
162
+ /* Progress bar */
163
+ .stProgress > div > div {
164
+ background: linear-gradient(90deg, var(--accent), var(--accent2)) !important;
165
+ border-radius: 99px !important;
166
+ }
167
+
168
+ /* Buttons */
169
+ .stButton > button {
170
+ background: linear-gradient(135deg, #6c47ff, #8b6eff) !important;
171
+ color: #fff !important;
172
+ font-weight: 600 !important;
173
+ border: none !important;
174
+ border-radius: 12px !important;
175
+ padding: 0.62rem 2rem !important;
176
+ font-family: 'Plus Jakarta Sans', sans-serif !important;
177
+ box-shadow: 0 4px 14px rgba(108,71,255,0.25) !important;
178
+ transition: opacity 0.18s, transform 0.18s !important;
179
+ }
180
+ .stButton > button:hover {
181
+ opacity: 0.9 !important;
182
+ transform: translateY(-1px) !important;
183
+ }
184
+
185
+ /* Inputs */
186
+ .stSelectbox > div > div,
187
+ .stTextInput > div > div,
188
+ .stTextArea > div > div,
189
+ .stNumberInput > div > div {
190
+ background: var(--card) !important;
191
+ border-color: var(--border) !important;
192
+ color: var(--text) !important;
193
+ border-radius: 12px !important;
194
+ box-shadow: var(--shadow) !important;
195
+ }
196
+
197
+ /* Radio */
198
+ .stRadio > div { gap: 0.5rem; }
199
+ .stRadio label {
200
+ background: var(--card) !important;
201
+ border: 1.5px solid var(--border) !important;
202
+ border-radius: 12px !important;
203
+ padding: 0.65rem 1.1rem !important;
204
+ transition: border-color 0.18s !important;
205
+ }
206
+ .stRadio label:hover { border-color: var(--accent) !important; }
207
+
208
+ /* Metrics */
209
+ [data-testid="stMetricValue"] {
210
+ color: var(--accent) !important;
211
+ font-family: 'Fraunces', serif !important;
212
+ font-size: 2rem !important;
213
+ }
214
+ [data-testid="stMetricLabel"] { color: var(--muted) !important; font-size: 0.82rem !important; }
215
+
216
+ /* Tabs */
217
+ .stTabs [data-baseweb="tab"] { color: var(--muted) !important; font-weight: 600 !important; }
218
+ .stTabs [aria-selected="true"] { color: var(--accent) !important; }
219
+ .stTabs [data-baseweb="tab-highlight"] { background: var(--accent) !important; }
220
+ .stTabs [data-baseweb="tab-border"] { background: var(--border) !important; }
221
+
222
+ /* Dividers */
223
+ hr { border-color: var(--border) !important; }
224
+
225
+ /* Alerts */
226
+ .stSuccess, .stWarning, .stError, .stInfo { border-radius: 12px !important; }
227
+
228
+ /* Dataframe */
229
+ [data-testid="stDataFrame"] { border-radius: 14px !important; overflow: hidden; box-shadow: var(--shadow); }
230
+
231
+ /* Flashcard flip */
232
+ .flip-card {
233
+ perspective: 900px;
234
+ height: 190px;
235
+ cursor: pointer;
236
+ margin: 0.5rem 0;
237
+ }
238
+ .flip-inner {
239
+ position: relative; width: 100%; height: 100%;
240
+ transition: transform 0.55s cubic-bezier(.4,2,.55,.44);
241
+ transform-style: preserve-3d;
242
+ }
243
+ .flip-card.flipped .flip-inner { transform: rotateY(180deg); }
244
+ .flip-front, .flip-back {
245
+ position: absolute; width: 100%; height: 100%;
246
+ backface-visibility: hidden;
247
+ border-radius: 16px;
248
+ display: flex; align-items: center; justify-content: center;
249
+ padding: 1.2rem;
250
+ text-align: center;
251
+ box-shadow: var(--shadow);
252
+ }
253
+ .flip-front {
254
+ background: var(--card);
255
+ border: 2px solid var(--border);
256
+ color: var(--text);
257
+ font-weight: 600; font-size: 1rem;
258
+ }
259
+ .flip-back {
260
+ background: linear-gradient(135deg, #6c47ff, #8b6eff);
261
+ border: 2px solid transparent;
262
+ color: #fff;
263
+ font-size: 0.92rem;
264
+ transform: rotateY(180deg);
265
+ }
266
+
267
+ /* Sidebar nav buttons */
268
+ [data-testid="stSidebar"] .stButton > button {
269
+ background: rgba(255,255,255,0.08) !important;
270
+ color: #e2e0f5 !important;
271
+ border: 1px solid rgba(255,255,255,0.12) !important;
272
+ text-align: left !important;
273
+ box-shadow: none !important;
274
+ border-radius: 10px !important;
275
+ }
276
+ [data-testid="stSidebar"] .stButton > button:hover {
277
+ background: rgba(255,255,255,0.18) !important;
278
+ transform: none !important;
279
+ }
280
+
281
+ /* Chat bubbles */
282
+ .chat-user {
283
+ background: linear-gradient(135deg, #6c47ff, #8b6eff);
284
+ color: #fff;
285
+ border-radius: 18px 18px 4px 18px;
286
+ padding: 0.75rem 1.1rem;
287
+ margin: 0.4rem 0 0.4rem 20%;
288
+ font-size: 0.95rem;
289
+ line-height: 1.55;
290
+ box-shadow: 0 2px 8px rgba(108,71,255,0.18);
291
+ }
292
+ .chat-ai {
293
+ background: #fff;
294
+ color: #1a1523;
295
+ border: 1.5px solid #e2ddf5;
296
+ border-radius: 18px 18px 18px 4px;
297
+ padding: 0.75rem 1.1rem;
298
+ margin: 0.4rem 20% 0.4rem 0;
299
+ font-size: 0.95rem;
300
+ line-height: 1.6;
301
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
302
+ }
303
+ .chat-label { font-size:0.72rem; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; margin-bottom:0.15rem; }
304
+ .badge-card { background:#fff; border:1.5px solid #e2ddf5; border-radius:14px; padding:1rem 0.8rem; text-align:center; box-shadow:0 2px 10px rgba(108,71,255,0.07); transition:transform 0.18s; }
305
+ .badge-card:hover { transform:translateY(-3px); }
306
+ .badge-card.earned { border-color:#6c47ff; background:#ede9fe; }
307
+ .badge-card.locked { opacity:0.45; filter:grayscale(0.6); }
308
+ .toast { background:linear-gradient(135deg,#6c47ff,#8b6eff); color:#fff; border-radius:14px; padding:0.9rem 1.3rem; margin:0.4rem 0; display:flex; align-items:center; gap:0.75rem; box-shadow:0 4px 18px rgba(108,71,255,0.25); }
309
+ </style>
310
+ """, unsafe_allow_html=True)
311
+
312
+ # ── Session state init ────────────────────────────────────────────────��───────
313
+ defaults = {
314
+ "page": "home",
315
+ "content": None,
316
+ "quiz": None,
317
+ "answers": {},
318
+ "submitted": False,
319
+ "progress": load_progress(),
320
+ "quiz_start_time": None,
321
+ "quiz_elapsed": None,
322
+ "flashcards": [],
323
+ "fc_index": 0,
324
+ "fc_flipped": False,
325
+ "daily_goal": 3,
326
+ "sessions_today": 0,
327
+ # Gamification
328
+ "gami": load_gamification(),
329
+ "new_badges": [],
330
+ "xp_gained": 0,
331
+ "level_up_msg": None,
332
+ # Tutor chat
333
+ "tutor_messages": [],
334
+ "tutor_topic": "",
335
+ }
336
+ for k, v in defaults.items():
337
+ if k not in st.session_state:
338
+ st.session_state[k] = v
339
+
340
+ # ── Sidebar ───────────────────────────────────────────────────────────────────
341
+ with st.sidebar:
342
+ st.markdown("""
343
+ <div style='padding:1.2rem 0 2rem 0;'>
344
+ <div style='font-size:2.4rem; margin-bottom:0.25rem;'>πŸŽ“</div>
345
+ <div style='font-family: Fraunces, serif; font-size:1.5rem; font-weight:700; color:#fff;'>LearnCraft</div>
346
+ <div style='color:rgba(226,224,245,0.6); font-size:0.82rem;'>Personalized Learning Platform</div>
347
+ </div>
348
+ """, unsafe_allow_html=True)
349
+
350
+ pages = {
351
+ "🏠 Home": "home",
352
+ "πŸ“š Study Content": "study",
353
+ "πŸƒ Flashcards": "flashcards",
354
+ "🧩 Take Quiz": "quiz",
355
+ "πŸ€– AI Tutor": "tutor",
356
+ "πŸ… Achievements": "achievements",
357
+ "πŸ“Š My Progress": "progress",
358
+ "πŸ“ My Notes": "notes",
359
+ }
360
+ for label, key in pages.items():
361
+ is_active = st.session_state.page == key
362
+ btn_label = f"β–Ά {label}" if is_active else label
363
+ if st.button(btn_label, key=f"nav_{key}", use_container_width=True):
364
+ st.session_state.page = key
365
+ st.session_state.submitted = False
366
+ st.rerun()
367
+
368
+ st.markdown("---")
369
+
370
+ # XP & Level display
371
+ gami = st.session_state.gami
372
+ xp = gami.get("xp", 0)
373
+ streak = gami.get("streak", 0)
374
+ level_name, next_level_name, xp_to_next, level_pct = get_level(xp)
375
+ badges_earned = len(gami.get("badges", []))
376
+
377
+ st.markdown(f"""
378
+ <div style='margin-bottom:0.6rem;'>
379
+ <div style='color:rgba(226,224,245,0.55); font-size:0.72rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.3rem;'>Your Level</div>
380
+ <div style='color:#fff; font-size:1.15rem; font-weight:700;'>{level_name}</div>
381
+ <div style='color:#a78bfa; font-size:0.8rem; margin-top:0.1rem;'>{xp} XP {f"Β· {xp_to_next} to {next_level_name}" if next_level_name else "Β· MAX LEVEL"}</div>
382
+ <div style='background:rgba(255,255,255,0.12); border-radius:99px; height:6px; margin-top:6px; overflow:hidden;'>
383
+ <div style='height:100%; width:{level_pct}%; background:linear-gradient(90deg,#a78bfa,#f97316); border-radius:99px;'></div>
384
+ </div>
385
+ </div>
386
+ """, unsafe_allow_html=True)
387
+
388
+ # Daily goal tracker
389
+ progress = st.session_state.progress
390
+ sessions_today = sum(
391
+ 1 for s in progress.get("sessions", [])
392
+ if s.get("date") == str(__import__("datetime").date.today())
393
+ )
394
+ goal = st.session_state.daily_goal
395
+ goal_pct = min(sessions_today / goal, 1.0)
396
+ st.markdown(f"""
397
+ <div style='margin-bottom:0.3rem;'>
398
+ <div style='color:rgba(226,224,245,0.55); font-size:0.75rem; font-weight:600; text-transform:uppercase; letter-spacing:0.05em;'>Today's Goal</div>
399
+ <div style='color:#fff; font-size:1.3rem; font-weight:700; margin:0.15rem 0;'>{sessions_today} / {goal} sessions</div>
400
+ </div>
401
+ """, unsafe_allow_html=True)
402
+ st.progress(goal_pct)
403
+
404
+ st.markdown("<div style='height:0.4rem'></div>", unsafe_allow_html=True)
405
+
406
+ topics_count = len(set(progress.get("topics_studied", [])))
407
+ best_score = progress.get("best_score", 0)
408
+ scores = progress.get("scores", [])
409
+ avg_score = round(sum(scores)/len(scores), 1) if scores else 0
410
+
411
+ st.markdown(f"""
412
+ <div style='display:flex; gap:0.4rem; margin-top:0.5rem; flex-wrap:wrap;'>
413
+ <div style='flex:1; min-width:48px; background:rgba(255,255,255,0.07); border-radius:10px; padding:0.5rem 0.4rem; text-align:center;'>
414
+ <div style='font-size:1.1rem; font-weight:700; color:#f97316;'>πŸ”₯{streak}</div>
415
+ <div style='font-size:0.63rem; color:rgba(226,224,245,0.55);'>Streak</div>
416
+ </div>
417
+ <div style='flex:1; min-width:48px; background:rgba(255,255,255,0.07); border-radius:10px; padding:0.5rem 0.4rem; text-align:center;'>
418
+ <div style='font-size:1.1rem; font-weight:700; color:#a78bfa;'>{topics_count}</div>
419
+ <div style='font-size:0.63rem; color:rgba(226,224,245,0.55);'>Topics</div>
420
+ </div>
421
+ <div style='flex:1; min-width:48px; background:rgba(255,255,255,0.07); border-radius:10px; padding:0.5rem 0.4rem; text-align:center;'>
422
+ <div style='font-size:1.1rem; font-weight:700; color:#fbbf24;'>πŸ…{badges_earned}</div>
423
+ <div style='font-size:0.63rem; color:rgba(226,224,245,0.55);'>Badges</div>
424
+ </div>
425
+ <div style='flex:1; min-width:48px; background:rgba(255,255,255,0.07); border-radius:10px; padding:0.5rem 0.4rem; text-align:center;'>
426
+ <div style='font-size:1.1rem; font-weight:700; color:#10b981;'>{best_score}%</div>
427
+ <div style='font-size:0.63rem; color:rgba(226,224,245,0.55);'>Best</div>
428
+ </div>
429
+ </div>
430
+ """, unsafe_allow_html=True)
431
+
432
+ # ── Page routing ──────────────────────────────────────────────────────────────
433
+ page = st.session_state.page
434
+
435
+ # ══════════════════════ HOME ══════════════════════════════════════════════════
436
+ if page == "home":
437
+ st.markdown("""
438
+ <div class='hero'>
439
+ <div class='tag'>AI-Powered Learning</div>
440
+ <h1>LearnCraft</h1>
441
+ <p>Generate personalized study material, flashcards & quizzes<br>tailored exactly to your level and learning goals.</p>
442
+ </div>
443
+ """, unsafe_allow_html=True)
444
+
445
+ weak = get_weak_topics(st.session_state.progress)
446
+ if weak:
447
+ st.warning(f"πŸ“Œ **Recommended Review:** You scored below 60% on: {', '.join(weak)}. Consider revisiting these topics!")
448
+
449
+ c1, c2, c3, c4 = st.columns(4)
450
+ features = [
451
+ ("πŸ“–", "Smart Content", "AI-generated study notes adapted to Beginner, Intermediate, or Advanced levels in multiple styles."),
452
+ ("πŸƒ", "Flashcards", "Flip-card revision sessions with auto-generated front/back cards. Perfect for memorisation."),
453
+ ("🧩", "Custom Quizzes", "MCQ, True/False, Fill-in-the-Blank and Short Answer quizzes with timed mode."),
454
+ ("πŸ“Š", "Analytics", "Score history charts, per-topic bests, streak tracking and weak-topic detection."),
455
+ ]
456
+ for col, (icon, title, desc) in zip([c1, c2, c3, c4], features):
457
+ with col:
458
+ st.markdown(f"""
459
+ <div class='learn-card'>
460
+ <div style='font-size:2rem; margin-bottom:0.5rem;'>{icon}</div>
461
+ <h3 style='font-size:1.1rem !important; margin-bottom:0.4rem;'>{title}</h3>
462
+ <p style='color:#6b6880; font-size:0.87rem; line-height:1.6; margin:0;'>{desc}</p>
463
+ </div>
464
+ """, unsafe_allow_html=True)
465
+
466
+ st.markdown("---")
467
+ st.markdown("### πŸš€ Quick Start")
468
+ qa, qb, qc = st.columns(3)
469
+ with qa:
470
+ if st.button("πŸ“š Generate Study Notes", use_container_width=True):
471
+ st.session_state.page = "study"; st.rerun()
472
+ with qb:
473
+ if st.button("πŸƒ Practice Flashcards", use_container_width=True):
474
+ st.session_state.page = "flashcards"; st.rerun()
475
+ with qc:
476
+ if st.button("🧩 Start a Quiz", use_container_width=True):
477
+ st.session_state.page = "quiz"; st.rerun()
478
+
479
+ # Recent activity
480
+ sessions = st.session_state.progress.get("sessions", [])
481
+ if sessions:
482
+ st.markdown("---")
483
+ st.markdown("### πŸ• Recent Activity")
484
+ recent = sessions[-5:][::-1]
485
+ for s in recent:
486
+ score = s.get("score", 0)
487
+ color = "#10b981" if score >= 80 else "#f97316" if score >= 60 else "#ef4444"
488
+ st.markdown(f"""
489
+ <div style='background:#fff; border:1px solid #e2ddf5; border-radius:12px; padding:0.75rem 1.2rem; margin:0.3rem 0; display:flex; align-items:center; justify-content:space-between; box-shadow:0 2px 8px rgba(108,71,255,0.06);'>
490
+ <div>
491
+ <span style='font-weight:600; color:#1a1523;'>{s.get("topic","Unknown")}</span>
492
+ <span style='color:#6b6880; font-size:0.82rem; margin-left:0.5rem;'>{s.get("date","")}</span>
493
+ </div>
494
+ <div style='font-weight:700; color:{color}; font-family:Fraunces,serif; font-size:1.1rem;'>{score}%</div>
495
+ </div>
496
+ """, unsafe_allow_html=True)
497
+
498
+ # ══════════════════════ STUDY ════════════════════════════════════════════════
499
+ elif page == "study":
500
+ st.markdown("## πŸ“š Study Content Generator")
501
+ st.markdown("<div style='color:#6b6880; margin-bottom:1rem;'>Choose a topic and level to get personalized AI-generated study notes.</div>", unsafe_allow_html=True)
502
+
503
+ col1, col2 = st.columns([2, 1])
504
+ with col1:
505
+ topic = st.text_input("πŸ“Œ Topic", placeholder="e.g. Photosynthesis, World War II, Python Functions…")
506
+ custom_focus = st.text_area("🎯 Focus area (optional)", placeholder="e.g. focus on the Calvin cycle, or key dates and battles…", height=75)
507
+ with col2:
508
+ level = st.selectbox("πŸŽ“ Your Level", ["Beginner", "Intermediate", "Advanced"])
509
+ content_type = st.selectbox("πŸ“„ Content Style", ["Summary Notes", "Detailed Explanation", "Bullet Points", "Concept Map"])
510
+
511
+ if topic:
512
+ path = get_learning_path(topic)
513
+ if path:
514
+ path_html = " β†’ ".join(
515
+ f"<span style='color:#6c47ff; font-weight:700;'>{s}</span>" if s.lower() == topic.lower()
516
+ else f"<span style='color:#6b6880;'>{s}</span>"
517
+ for s in path
518
+ )
519
+ st.markdown(f"""
520
+ <div style='background:#fff; border:1px solid #e2ddf5; border-radius:12px; padding:0.8rem 1.3rem; margin-bottom:1rem; box-shadow:0 2px 8px rgba(108,71,255,0.06);'>
521
+ πŸ—ΊοΈ <strong style='color:#1a1523;'>Suggested Path:</strong> &nbsp;{path_html}
522
+ </div>
523
+ """, unsafe_allow_html=True)
524
+
525
+ if st.button("✨ Generate Content", use_container_width=True):
526
+ if not topic.strip():
527
+ st.warning("Please enter a topic first.")
528
+ else:
529
+ with st.spinner("Crafting your personalized content…"):
530
+ content = generate_content(topic, level, content_type, custom_focus)
531
+ st.session_state.content = content
532
+ save_progress(st.session_state.progress, topic=topic.title())
533
+ # Gamification: award XP for study session
534
+ gami = update_streak(st.session_state.gami)
535
+ gami, xp_amt, lvl_up = award_xp(gami, XP_STUDY_SESSION, "study_session")
536
+ topics_count = len(set(st.session_state.progress.get("topics_studied", [])))
537
+ new_b = check_and_award_badges(gami, {"topics_count": topics_count, "event": ""})
538
+ save_gamification(gami)
539
+ st.session_state.gami = gami
540
+ if new_b:
541
+ st.session_state.new_badges = new_b
542
+ if lvl_up:
543
+ st.session_state.level_up_msg = lvl_up
544
+
545
+ if st.session_state.content:
546
+ st.markdown("---")
547
+ data = st.session_state.content
548
+
549
+ m1, m2, m3 = st.columns(3)
550
+ m1.metric("Topic", data["topic"])
551
+ m2.metric("Level", data["level"])
552
+ m3.metric("Est. Read Time", data["read_time"])
553
+
554
+ if data.get("ai_generated"):
555
+ st.info("✨ Content is AI-generated and tailored to your topic and level.")
556
+
557
+ for section in data["sections"]:
558
+ st.markdown(f"""
559
+ <div class='content-block'>
560
+ <h3>πŸ“Œ {section['title']}</h3>
561
+ <div>{section['content']}</div>
562
+ </div>
563
+ """, unsafe_allow_html=True)
564
+
565
+ if data.get("key_terms"):
566
+ st.markdown("### πŸ”‘ Key Terms")
567
+ cols = st.columns(3)
568
+ for i, term in enumerate(data["key_terms"]):
569
+ with cols[i % 3]:
570
+ st.markdown(f"""
571
+ <div style='background:#f7f4ef; border:1px solid #e2ddf5; border-radius:10px; padding:0.7rem 1rem; margin:0.3rem 0;'>
572
+ <div style='color:#6c47ff; font-weight:700; font-size:0.9rem;'>{term['term']}</div>
573
+ <div style='color:#6b6880; font-size:0.82rem; margin-top:0.2rem;'>{term['definition']}</div>
574
+ </div>
575
+ """, unsafe_allow_html=True)
576
+
577
+ if data.get("summary"):
578
+ st.markdown("### πŸ’‘ Quick Summary")
579
+ st.info(data["summary"])
580
+
581
+ st.markdown("---")
582
+ col_note, col_quiz, col_flash = st.columns(3)
583
+ with col_note:
584
+ st.markdown("##### πŸ“ Save a Note")
585
+ note_text = st.text_area("Note", placeholder="Something to remember…", height=75, label_visibility="collapsed")
586
+ if st.button("πŸ’Ύ Save Note"):
587
+ if note_text.strip():
588
+ save_note(note_text, data["topic"])
589
+ # Badge for first note
590
+ gami = st.session_state.gami
591
+ new_b = check_and_award_badges(gami, {"event": "note_saved"})
592
+ if new_b:
593
+ save_gamification(gami)
594
+ st.session_state.gami = gami
595
+ st.session_state.new_badges = new_b
596
+ st.success("Note saved!")
597
+ else:
598
+ st.warning("Write something first.")
599
+ with col_quiz:
600
+ st.markdown("##### 🧩 Test Yourself")
601
+ st.markdown("<div style='color:#6b6880; font-size:0.88rem; margin-bottom:0.6rem;'>Take a quiz on this exact topic.</div>", unsafe_allow_html=True)
602
+ if st.button("🧩 Start Quiz on This Topic", use_container_width=True):
603
+ st.session_state.page = "quiz"
604
+ st.session_state.quiz_topic = data["topic"]
605
+ st.session_state.quiz_level = data["level"]
606
+ st.session_state.submitted = False
607
+ st.rerun()
608
+ with col_flash:
609
+ st.markdown("##### πŸƒ Flashcard Mode")
610
+ st.markdown("<div style='color:#6b6880; font-size:0.88rem; margin-bottom:0.6rem;'>Generate flip-cards for quick revision.</div>", unsafe_allow_html=True)
611
+ if st.button("πŸƒ Generate Flashcards", use_container_width=True):
612
+ st.session_state.fc_topic = data["topic"]
613
+ st.session_state.fc_level = data["level"]
614
+ st.session_state.page = "flashcards"
615
+ st.rerun()
616
+
617
+ # PDF Export
618
+ st.markdown("---")
619
+ st.markdown("##### πŸ“„ Export as PDF")
620
+ if st.button("⬇️ Download Study Notes PDF", use_container_width=True):
621
+ with st.spinner("Generating PDF…"):
622
+ pdf_bytes = export_study_notes_pdf(data)
623
+ st.download_button(
624
+ label="πŸ“₯ Click to Download PDF",
625
+ data=pdf_bytes,
626
+ file_name=f"LearnCraft_{data['topic'].replace(' ','_')}_Notes.pdf",
627
+ mime="application/pdf",
628
+ use_container_width=True,
629
+ )
630
+
631
+ # ══════════════════════ FLASHCARDS ══════════════════════════════════════════
632
+ elif page == "flashcards":
633
+ st.markdown("## πŸƒ Flashcard Studio")
634
+ st.markdown("<div style='color:#6b6880; margin-bottom:1rem;'>Click any card to flip it and reveal the answer.</div>", unsafe_allow_html=True)
635
+
636
+ col1, col2 = st.columns([2, 1])
637
+ with col1:
638
+ default_topic = getattr(st.session_state, "fc_topic", "")
639
+ fc_topic = st.text_input("πŸ“Œ Topic", value=default_topic, placeholder="e.g. Quantum Mechanics, World War II…")
640
+ with col2:
641
+ default_level = getattr(st.session_state, "fc_level", "Intermediate")
642
+ fc_level = st.selectbox("πŸŽ“ Level", ["Beginner", "Intermediate", "Advanced"],
643
+ index=["Beginner", "Intermediate", "Advanced"].index(default_level))
644
+ num_cards = st.slider("Number of Cards", min_value=5, max_value=20, value=10)
645
+
646
+ if st.button("πŸƒ Generate Flashcards", use_container_width=True):
647
+ if not fc_topic.strip():
648
+ st.warning("Please enter a topic.")
649
+ else:
650
+ with st.spinner("Creating your flashcard set…"):
651
+ cards = generate_flashcards(fc_topic, fc_level, num_cards)
652
+ st.session_state.flashcards = cards
653
+ st.session_state.fc_index = 0
654
+ st.session_state.fc_flipped = False
655
+ # Gamification: XP for flashcard deck
656
+ gami = update_streak(st.session_state.gami)
657
+ gami, xp_amt, lvl_up = award_xp(gami, XP_FLASHCARD_DECK, "flashcard_deck")
658
+ new_b = check_and_award_badges(gami, {"event": "flashcards"})
659
+ save_gamification(gami)
660
+ st.session_state.gami = gami
661
+ if new_b:
662
+ st.session_state.new_badges = new_b
663
+ if lvl_up:
664
+ st.session_state.level_up_msg = lvl_up
665
+
666
+ cards = st.session_state.flashcards
667
+ if cards:
668
+ st.markdown("---")
669
+ idx = st.session_state.fc_index
670
+ total_fc = len(cards)
671
+ card = cards[idx]
672
+ flipped = st.session_state.fc_flipped
673
+
674
+ # Progress
675
+ st.markdown(f"""
676
+ <div style='display:flex; justify-content:space-between; align-items:center; margin-bottom:0.8rem;'>
677
+ <div style='color:#6b6880; font-size:0.88rem; font-weight:600;'>Card {idx+1} of {total_fc}</div>
678
+ <div style='color:#6c47ff; font-size:0.88rem; font-weight:600;'>{round((idx+1)/total_fc*100)}% through deck</div>
679
+ </div>
680
+ """, unsafe_allow_html=True)
681
+ st.progress((idx + 1) / total_fc)
682
+
683
+ # Flip card (CSS-based)
684
+ flipped_class = "flipped" if flipped else ""
685
+ st.markdown(f"""
686
+ <div class="flip-card {flipped_class}" id="fc-main" onclick="this.classList.toggle('flipped')">
687
+ <div class="flip-inner">
688
+ <div class="flip-front">
689
+ <div>
690
+ <div style='font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.08em; color:#6b6880; margin-bottom:0.5rem;'>QUESTION</div>
691
+ <div style='font-size:1.05rem; font-weight:600; color:#1a1523;'>{card['front']}</div>
692
+ </div>
693
+ </div>
694
+ <div class="flip-back">
695
+ <div>
696
+ <div style='font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.08em; color:rgba(255,255,255,0.7); margin-bottom:0.5rem;'>ANSWER</div>
697
+ <div style='font-size:0.95rem;'>{card['back']}</div>
698
+ </div>
699
+ </div>
700
+ </div>
701
+ </div>
702
+ """, unsafe_allow_html=True)
703
+
704
+ st.markdown("")
705
+ nav1, nav2, nav3 = st.columns([1, 2, 1])
706
+ with nav1:
707
+ if st.button("β¬… Previous", use_container_width=True, disabled=(idx == 0)):
708
+ st.session_state.fc_index = idx - 1
709
+ st.session_state.fc_flipped = False
710
+ st.rerun()
711
+ with nav2:
712
+ if st.button("πŸ”„ Flip Card", use_container_width=True):
713
+ st.session_state.fc_flipped = not st.session_state.fc_flipped
714
+ st.rerun()
715
+ with nav3:
716
+ if st.button("Next ➑", use_container_width=True, disabled=(idx == total_fc - 1)):
717
+ st.session_state.fc_index = idx + 1
718
+ st.session_state.fc_flipped = False
719
+ st.rerun()
720
+
721
+ # Mini deck overview
722
+ st.markdown("---")
723
+ st.markdown("### πŸ“‹ All Cards in This Deck")
724
+ for i, c in enumerate(cards):
725
+ bg = "#ede9fe" if i == idx else "#fff"
726
+ bd = "#6c47ff" if i == idx else "#e2ddf5"
727
+ st.markdown(f"""
728
+ <div style='background:{bg}; border:1.5px solid {bd}; border-radius:10px; padding:0.6rem 1rem; margin:0.3rem 0; display:flex; justify-content:space-between;'>
729
+ <div style='color:#1a1523; font-size:0.88rem; font-weight:600;'>{i+1}. {c['front']}</div>
730
+ </div>
731
+ """, unsafe_allow_html=True)
732
+
733
+ # ══════════════════════ QUIZ ════════════════════════════════════════════════
734
+ elif page == "quiz":
735
+ st.markdown("## 🧩 Quiz Generator")
736
+
737
+ if not st.session_state.submitted:
738
+ st.markdown("<div style='color:#6b6880; margin-bottom:1rem;'>Configure your quiz, then test your knowledge!</div>", unsafe_allow_html=True)
739
+
740
+ col1, col2 = st.columns([2, 1])
741
+ with col1:
742
+ default_topic = getattr(st.session_state, "quiz_topic", "")
743
+ topic = st.text_input("πŸ“Œ Topic", value=default_topic, placeholder="e.g. Machine Learning, Calculus…")
744
+ with col2:
745
+ default_level = getattr(st.session_state, "quiz_level", "Intermediate")
746
+ level = st.selectbox("πŸŽ“ Difficulty", ["Beginner", "Intermediate", "Advanced"],
747
+ index=["Beginner", "Intermediate", "Advanced"].index(default_level))
748
+
749
+ col3, col4, col5 = st.columns(3)
750
+ with col3:
751
+ num_q = st.number_input("Number of Questions", min_value=3, max_value=15, value=5)
752
+ with col4:
753
+ q_type = st.selectbox("Question Type", ["Mixed", "Multiple Choice", "True/False", "Short Answer", "Fill in the Blank"])
754
+ with col5:
755
+ time_limit = st.selectbox("⏱️ Time Limit", ["No limit", "5 minutes", "10 minutes", "15 minutes"])
756
+
757
+ if st.button("🎲 Generate Quiz", use_container_width=True):
758
+ if not topic.strip():
759
+ st.warning("Please enter a topic.")
760
+ else:
761
+ with st.spinner("Building your quiz…"):
762
+ quiz = generate_quiz(topic, level, num_q, q_type)
763
+ st.session_state.quiz = quiz
764
+ st.session_state.answers = {}
765
+ st.session_state.submitted = False
766
+ st.session_state.quiz_start_time = time.time()
767
+ st.session_state.time_limit = time_limit
768
+ st.session_state.quiz_elapsed = None
769
+
770
+ if st.session_state.quiz and not st.session_state.submitted:
771
+ quiz = st.session_state.quiz
772
+
773
+ if st.session_state.quiz_start_time:
774
+ elapsed = int(time.time() - st.session_state.quiz_start_time)
775
+ limit = st.session_state.get("time_limit", "No limit")
776
+ if limit != "No limit":
777
+ limit_secs = int(limit.split()[0]) * 60
778
+ remaining = limit_secs - elapsed
779
+ if remaining <= 0:
780
+ st.error("⏱️ Time's up! Submitting…")
781
+ st.session_state.quiz_elapsed = elapsed
782
+ st.session_state.submitted = True
783
+ st.rerun()
784
+ else:
785
+ r_m, r_s = divmod(remaining, 60)
786
+ st.info(f"⏱️ Time remaining: {r_m}m {r_s}s")
787
+ else:
788
+ m, s = divmod(elapsed, 60)
789
+ st.info(f"⏱️ Elapsed: {m}m {s}s")
790
+
791
+ st.markdown("---")
792
+ st.markdown(f"### πŸ“ {quiz['title']}")
793
+ st.markdown(f"<div style='color:#6b6880; margin-bottom:1.5rem;'>{len(quiz['questions'])} questions Β· {quiz['difficulty']} Β· {quiz['topic']}</div>", unsafe_allow_html=True)
794
+
795
+ for i, q in enumerate(quiz["questions"]):
796
+ st.markdown(f"""
797
+ <div class='learn-card'>
798
+ <div style='color:#6b6880; font-size:0.75rem; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; margin-bottom:0.5rem;'>Question {i+1} Β· {q['type']}</div>
799
+ <div style='font-size:1.02rem; font-weight:600; color:#1a1523; margin-bottom:1rem;'>{q['question']}</div>
800
+ </div>
801
+ """, unsafe_allow_html=True)
802
+
803
+ if q["type"] in ("Multiple Choice", "True/False"):
804
+ ans = st.radio(f"Answer Q{i+1}", q["options"], key=f"q_{i}", label_visibility="collapsed")
805
+ st.session_state.answers[i] = ans
806
+ elif q["type"] == "Fill in the Blank":
807
+ ans = st.text_input(f"Blank Q{i+1}", key=f"q_{i}", label_visibility="collapsed", placeholder="Type the missing word…")
808
+ st.session_state.answers[i] = ans
809
+ else:
810
+ ans = st.text_input(f"Answer Q{i+1}", key=f"q_{i}", label_visibility="collapsed", placeholder="Type your answer…")
811
+ st.session_state.answers[i] = ans
812
+
813
+ st.markdown("")
814
+ if st.button("βœ… Submit Quiz", use_container_width=True):
815
+ st.session_state.quiz_elapsed = int(time.time() - st.session_state.quiz_start_time)
816
+ st.session_state.submitted = True
817
+ st.rerun()
818
+
819
+ else:
820
+ # ── Results ──────────────────────────────────────────────────────────
821
+ quiz = st.session_state.quiz
822
+ results = evaluate_answers(quiz, st.session_state.answers)
823
+ score = results["score_percent"]
824
+ elapsed = st.session_state.get("quiz_elapsed", 0) or 0
825
+ em, es = divmod(elapsed, 60)
826
+
827
+ st.markdown(f"""
828
+ <div class='hero'>
829
+ <div class='tag'>Quiz Complete</div>
830
+ <div class='score-badge'>{score}%</div>
831
+ <div style='color:rgba(255,255,255,0.85); margin-top:0.4rem;'>{results['correct']} / {results['total']} correct</div>
832
+ <div style='color:rgba(255,255,255,0.7); font-size:0.9rem; margin-top:0.3rem;'>⏱️ Completed in {em}m {es}s</div>
833
+ <div style='margin-top:1rem; font-size:1.1rem; color:#fff;'>{results['feedback']}</div>
834
+ </div>
835
+ """, unsafe_allow_html=True)
836
+
837
+ save_progress(st.session_state.progress, score=score, topic=quiz["topic"])
838
+
839
+ # Gamification: award XP for quiz
840
+ gami = update_streak(st.session_state.gami)
841
+ xp_for_quiz = get_xp_for_quiz(score)
842
+ gami, xp_amt, lvl_up = award_xp(gami, xp_for_quiz)
843
+ gami = record_quiz(gami, score, len(set(st.session_state.progress.get("topics_studied", []))))
844
+ topics_count = len(set(st.session_state.progress.get("topics_studied", [])))
845
+ quizzes_count = gami.get("total_quizzes", 0)
846
+ new_b = check_and_award_badges(gami, {
847
+ "score": score, "topics_count": topics_count,
848
+ "quizzes_count": quizzes_count, "event": "quiz",
849
+ })
850
+ save_gamification(gami)
851
+ st.session_state.gami = gami
852
+ if new_b:
853
+ st.session_state.new_badges = new_b
854
+ if lvl_up:
855
+ st.session_state.level_up_msg = lvl_up
856
+
857
+ # Show XP toast
858
+ st.markdown(f"""
859
+ <div class='toast'>
860
+ <div style='font-size:1.5rem;'>⚑</div>
861
+ <div>
862
+ <div style='font-weight:700; font-size:0.95rem;'>+{xp_for_quiz} XP earned!</div>
863
+ <div style='font-size:0.82rem; opacity:0.85;'>Total: {gami.get("xp",0)} XP Β· {get_level(gami.get("xp",0))[0]}</div>
864
+ </div>
865
+ </div>
866
+ """, unsafe_allow_html=True)
867
+
868
+ # Show new badges
869
+ if st.session_state.new_badges:
870
+ for bk in st.session_state.new_badges:
871
+ b = BADGES.get(bk, {})
872
+ st.markdown(f"""
873
+ <div class='toast' style='background:linear-gradient(135deg,#f97316,#fbbf24);'>
874
+ <div style='font-size:1.5rem;'>{b.get("icon","πŸ…")}</div>
875
+ <div>
876
+ <div style='font-weight:700; font-size:0.95rem;'>Badge Unlocked: {b.get("name","")}</div>
877
+ <div style='font-size:0.82rem; opacity:0.85;'>{b.get("desc","")}</div>
878
+ </div>
879
+ </div>
880
+ """, unsafe_allow_html=True)
881
+ st.session_state.new_badges = []
882
+
883
+ if lvl_up:
884
+ st.balloons()
885
+ st.success(f"πŸš€ Level Up! You reached **{lvl_up}**!")
886
+ st.session_state.level_up_msg = None
887
+
888
+ if score < 60:
889
+ st.warning(f"πŸ“Œ Score below 60%. We recommend revisiting **{quiz['topic']}**.")
890
+
891
+ # Score meter
892
+ col_meter = st.columns([1, 2, 1])[1]
893
+ with col_meter:
894
+ level_label = "Excellent 🌟" if score == 100 else "Great πŸŽ‰" if score >= 80 else "Good πŸ‘" if score >= 60 else "Fair πŸ“–" if score >= 40 else "Keep Going πŸ’ͺ"
895
+ st.markdown(f"<div style='text-align:center; color:#6c47ff; font-weight:700; margin-bottom:0.3rem;'>{level_label}</div>", unsafe_allow_html=True)
896
+ st.progress(score / 100)
897
+
898
+ st.markdown("### πŸ“‹ Answer Review")
899
+ for i, q in enumerate(quiz["questions"]):
900
+ correct = results["details"][i]["correct"]
901
+ user_ans = st.session_state.answers.get(i, "")
902
+ color = "#10b981" if correct else "#ef4444"
903
+ icon = "βœ…" if correct else "❌"
904
+ st.markdown(f"""
905
+ <div style='background:#fff; border:1px solid {color}33; border-left:4px solid {color}; border-radius:0 14px 14px 0; padding:1.2rem 1.5rem; margin:0.6rem 0; box-shadow:0 2px 8px rgba(0,0,0,0.04);'>
906
+ <div style='font-weight:700; margin-bottom:0.5rem; color:#1a1523;'>{icon} Q{i+1}: {q['question']}</div>
907
+ <div style='font-size:0.88rem; color:#6b6880;'>Your answer: <span style='color:{color}; font-weight:600;'>{user_ans if user_ans else "No answer"}</span></div>
908
+ <div style='font-size:0.88rem; color:#6b6880;'>Correct: <span style='color:#10b981; font-weight:600;'>{results["details"][i]["correct_answer"]}</span></div>
909
+ {f'<div style="font-size:0.82rem; color:#6b6880; margin-top:0.35rem; font-style:italic;">{results["details"][i]["explanation"]}</div>' if results["details"][i].get("explanation") else ""}
910
+ </div>
911
+ """, unsafe_allow_html=True)
912
+
913
+ st.markdown("")
914
+ c1, c2, c3 = st.columns(3)
915
+ with c1:
916
+ if st.button("πŸ” Retake Quiz", use_container_width=True):
917
+ st.session_state.submitted = False
918
+ st.session_state.answers = {}
919
+ st.session_state.quiz_start_time = time.time()
920
+ st.rerun()
921
+ with c2:
922
+ if st.button("πŸ“š Study This Topic", use_container_width=True):
923
+ st.session_state.page = "study"
924
+ st.session_state.submitted = False
925
+ st.rerun()
926
+ with c3:
927
+ if st.button("πŸƒ Flashcard Revision", use_container_width=True):
928
+ st.session_state.fc_topic = quiz["topic"]
929
+ st.session_state.fc_level = quiz["difficulty"]
930
+ st.session_state.page = "flashcards"
931
+ st.session_state.submitted = False
932
+ st.rerun()
933
+
934
+ # PDF Export for quiz results
935
+ st.markdown("---")
936
+ if st.button("⬇️ Download Quiz Results PDF", use_container_width=True):
937
+ with st.spinner("Generating PDF…"):
938
+ pdf_bytes = export_quiz_results_pdf(quiz, results, st.session_state.answers)
939
+ st.download_button(
940
+ label="πŸ“₯ Click to Download Results PDF",
941
+ data=pdf_bytes,
942
+ file_name=f"LearnCraft_{quiz['topic'].replace(' ','_')}_Results.pdf",
943
+ mime="application/pdf",
944
+ use_container_width=True,
945
+ )
946
+
947
+ # ══════════════════════ PROGRESS ════════════════════════════════════════════
948
+ elif page == "progress":
949
+ st.markdown("## πŸ“Š My Learning Progress")
950
+ progress = st.session_state.progress
951
+ scores = progress.get("scores", [])
952
+ sessions = progress.get("sessions", [])
953
+
954
+ c1, c2, c3, c4 = st.columns(4)
955
+ topics_list = list(set(progress.get("topics_studied", [])))
956
+ avg = round(sum(scores)/len(scores), 1) if scores else 0
957
+ c1.metric("πŸ“š Topics Studied", len(topics_list))
958
+ c2.metric("πŸ† Best Score", f"{progress.get('best_score', 0)}%")
959
+ c3.metric("πŸ“ˆ Avg Score", f"{avg}%")
960
+ c4.metric("🎯 Total Quizzes", len(scores))
961
+
962
+ st.markdown("---")
963
+
964
+ if sessions:
965
+ st.markdown("### πŸ“ˆ Score History")
966
+ df = pd.DataFrame(sessions)
967
+ df.index = range(1, len(df) + 1)
968
+ df.index.name = "Quiz #"
969
+ st.line_chart(df[["score"]].rename(columns={"score": "Score (%)"}), color="#6c47ff")
970
+
971
+ topic_scores = progress.get("topic_scores", {})
972
+ if topic_scores:
973
+ st.markdown("### πŸ… Best Score Per Topic")
974
+ ts_df = pd.DataFrame([
975
+ {"Topic": t, "Best Score (%)": s, "Status": "βœ… Passing" if s >= 60 else "⚠️ Needs Review"}
976
+ for t, s in sorted(topic_scores.items(), key=lambda x: -x[1])
977
+ ])
978
+ st.dataframe(ts_df, use_container_width=True, hide_index=True)
979
+
980
+ weak = get_weak_topics(progress)
981
+ if weak:
982
+ st.markdown("### ⚠️ Topics to Improve")
983
+ for t in weak:
984
+ st.markdown(f"""
985
+ <div style='background:#fff; border:1px solid #ef444433; border-left:4px solid #ef4444; border-radius:0 12px 12px 0; padding:0.65rem 1.1rem; margin:0.35rem 0; color:#ef4444; font-weight:600;'>
986
+ ⚠️ {t} β€” below 60%
987
+ </div>
988
+ """, unsafe_allow_html=True)
989
+
990
+ if topics_list:
991
+ st.markdown("### πŸ—‚οΈ Topics Covered")
992
+ cols = st.columns(3)
993
+ for i, t in enumerate(topics_list):
994
+ with cols[i % 3]:
995
+ best = topic_scores.get(t, 0)
996
+ color = "#10b981" if best >= 80 else "#f97316" if best >= 60 else "#ef4444"
997
+ st.markdown(f"""
998
+ <div style='background:#fff; border:1px solid #e2ddf5; border-radius:10px; padding:0.7rem 1rem; margin:0.3rem 0; display:flex; justify-content:space-between; align-items:center;'>
999
+ <div style='color:#1a1523; font-weight:600; font-size:0.9rem;'>πŸ“Œ {t}</div>
1000
+ <div style='color:{color}; font-weight:700; font-size:0.9rem;'>{best}%</div>
1001
+ </div>
1002
+ """, unsafe_allow_html=True)
1003
+
1004
+ if not topics_list and not scores:
1005
+ st.markdown("""
1006
+ <div style='text-align:center; padding:4rem 2rem; color:#6b6880;'>
1007
+ <div style='font-size:3rem; margin-bottom:1rem;'>🌱</div>
1008
+ <div style='font-size:1.2rem;'>No activity yet. Start learning to see your progress!</div>
1009
+ </div>
1010
+ """, unsafe_allow_html=True)
1011
+ if st.button("πŸ“š Start Learning Now", use_container_width=True):
1012
+ st.session_state.page = "study"; st.rerun()
1013
+
1014
+ st.markdown("")
1015
+ if st.button("πŸ—‘οΈ Reset All Progress", type="secondary"):
1016
+ st.session_state.progress = {"topics_studied": [], "scores": [], "best_score": 0, "sessions": [], "topic_scores": {}}
1017
+ save_progress(st.session_state.progress)
1018
+ st.success("Progress reset.")
1019
+ st.rerun()
1020
+
1021
+ # ══════════════════════ NOTES ═══════════════════════════════════════════════
1022
+ elif page == "notes":
1023
+ st.markdown("## πŸ“ My Notes")
1024
+ st.markdown("<div style='color:#6b6880; margin-bottom:1rem;'>Notes you've saved while studying.</div>", unsafe_allow_html=True)
1025
+
1026
+ notes = load_notes()
1027
+
1028
+ if not notes:
1029
+ st.markdown("""
1030
+ <div style='text-align:center; padding:4rem 2rem; color:#6b6880;'>
1031
+ <div style='font-size:3rem; margin-bottom:1rem;'>πŸ“­</div>
1032
+ <div style='font-size:1.2rem;'>No notes yet. Save notes from the Study page!</div>
1033
+ </div>
1034
+ """, unsafe_allow_html=True)
1035
+ if st.button("πŸ“š Go to Study", use_container_width=True):
1036
+ st.session_state.page = "study"; st.rerun()
1037
+ else:
1038
+ # Search filter
1039
+ search = st.text_input("πŸ” Search notes", placeholder="Filter by keyword…")
1040
+ filtered = [n for n in reversed(notes) if not search or search.lower() in n.get("note","").lower() or search.lower() in n.get("topic","").lower()]
1041
+ st.markdown(f"**{len(filtered)} note(s)**")
1042
+
1043
+ for i, note in enumerate(filtered):
1044
+ actual_index = notes.index(note) if note in notes else -1
1045
+ col_note, col_del = st.columns([11, 1])
1046
+ with col_note:
1047
+ st.markdown(f"""
1048
+ <div style='background:#fff; border:1px solid #e2ddf5; border-left:4px solid #6c47ff; border-radius:0 14px 14px 0; padding:1rem 1.5rem; margin:0.5rem 0; box-shadow:0 2px 8px rgba(108,71,255,0.06);'>
1049
+ <div style='color:#6c47ff; font-size:0.78rem; font-weight:700; margin-bottom:0.4rem;'>πŸ“Œ {note.get("topic","Unknown")} Β· {note.get("date","")}</div>
1050
+ <div style='color:#1a1523; font-size:0.95rem; line-height:1.65;'>{note.get("note","")}</div>
1051
+ </div>
1052
+ """, unsafe_allow_html=True)
1053
+ with col_del:
1054
+ if st.button("πŸ—‘οΈ", key=f"del_{i}", help="Delete note"):
1055
+ if actual_index >= 0:
1056
+ delete_note(actual_index)
1057
+ st.rerun()
1058
+
1059
+ # ══════════════════════ AI TUTOR ═════════════════════════════════════════════
1060
+ elif page == "tutor":
1061
+ st.markdown("## πŸ€– AI Tutor")
1062
+ st.markdown("<div style='color:#6b6880; margin-bottom:1rem;'>Ask anything about any topic. Your tutor remembers the full conversation.</div>", unsafe_allow_html=True)
1063
+
1064
+ # Topic context selector
1065
+ col_t, col_c = st.columns([3, 1])
1066
+ with col_t:
1067
+ tutor_topic = st.text_input(
1068
+ "πŸ“Œ Topic context (optional)",
1069
+ value=st.session_state.tutor_topic,
1070
+ placeholder="e.g. Quantum Mechanics β€” helps the tutor stay focused",
1071
+ )
1072
+ st.session_state.tutor_topic = tutor_topic
1073
+ with col_c:
1074
+ st.markdown("<div style='height:1.85rem'></div>", unsafe_allow_html=True)
1075
+ if st.button("πŸ—‘οΈ Clear Chat", use_container_width=True):
1076
+ st.session_state.tutor_messages = []
1077
+ st.rerun()
1078
+
1079
+ st.markdown("---")
1080
+
1081
+ # Render conversation history
1082
+ msgs = st.session_state.tutor_messages
1083
+ if not msgs:
1084
+ st.markdown("""
1085
+ <div style='text-align:center; padding:3rem 2rem; color:#6b6880;'>
1086
+ <div style='font-size:3rem; margin-bottom:0.75rem;'>πŸ€–</div>
1087
+ <div style='font-size:1.1rem; font-weight:600; color:#1a1523; margin-bottom:0.4rem;'>Hi! I'm your LearnCraft Tutor.</div>
1088
+ <div style='font-size:0.95rem;'>Ask me anything about your topic β€” concepts, examples, quick tests, or explanations.</div>
1089
+ </div>
1090
+ """, unsafe_allow_html=True)
1091
+
1092
+ # Quick-start prompts
1093
+ st.markdown("#### πŸ’‘ Try asking:")
1094
+ prompts = [
1095
+ "Explain this topic like I'm 10 years old",
1096
+ "Give me 3 real-world examples",
1097
+ "What are the most common mistakes beginners make?",
1098
+ "Quiz me with one question",
1099
+ ]
1100
+ p_cols = st.columns(2)
1101
+ for i, prompt in enumerate(prompts):
1102
+ with p_cols[i % 2]:
1103
+ if st.button(f'"{prompt}"', key=f"prompt_{i}", use_container_width=True):
1104
+ full_prompt = prompt + (f" about {tutor_topic}" if tutor_topic else "")
1105
+ st.session_state.tutor_messages.append({"role": "user", "content": full_prompt})
1106
+ with st.spinner("Thinking…"):
1107
+ reply = get_tutor_reply(st.session_state.tutor_messages, tutor_topic)
1108
+ st.session_state.tutor_messages.append({"role": "assistant", "content": reply})
1109
+ st.rerun()
1110
+ else:
1111
+ for msg in msgs:
1112
+ if msg["role"] == "user":
1113
+ st.markdown(f"""
1114
+ <div style='text-align:right; margin-bottom:0.15rem;'>
1115
+ <span style='font-size:0.72rem; font-weight:700; color:#6b6880; text-transform:uppercase; letter-spacing:0.05em;'>You</span>
1116
+ </div>
1117
+ <div class='chat-user'>{msg['content']}</div>
1118
+ """, unsafe_allow_html=True)
1119
+ else:
1120
+ st.markdown(f"""
1121
+ <div style='margin-bottom:0.15rem;'>
1122
+ <span style='font-size:0.72rem; font-weight:700; color:#6c47ff; text-transform:uppercase; letter-spacing:0.05em;'>πŸ€– Tutor</span>
1123
+ </div>
1124
+ <div class='chat-ai'>{msg['content']}</div>
1125
+ """, unsafe_allow_html=True)
1126
+
1127
+ # Input box
1128
+ st.markdown("<div style='height:1rem'></div>", unsafe_allow_html=True)
1129
+ with st.form("chat_form", clear_on_submit=True):
1130
+ user_input = st.text_input(
1131
+ "Your question",
1132
+ placeholder="Ask your tutor anything…",
1133
+ label_visibility="collapsed",
1134
+ )
1135
+ submitted = st.form_submit_button("Send ➀", use_container_width=True)
1136
+
1137
+ if submitted and user_input.strip():
1138
+ st.session_state.tutor_messages.append({"role": "user", "content": user_input.strip()})
1139
+ with st.spinner("Tutor is thinking…"):
1140
+ reply = get_tutor_reply(st.session_state.tutor_messages, tutor_topic)
1141
+ st.session_state.tutor_messages.append({"role": "assistant", "content": reply})
1142
+ st.rerun()
1143
+
1144
+ # ══════════════════════ ACHIEVEMENTS ════════════════════════════════════════
1145
+ elif page == "achievements":
1146
+ gami = st.session_state.gami
1147
+ xp = gami.get("xp", 0)
1148
+ streak = gami.get("streak", 0)
1149
+ earned = set(gami.get("badges", []))
1150
+ level_name, next_level_name, xp_to_next, level_pct = get_level(xp)
1151
+
1152
+ st.markdown("## πŸ… Achievements")
1153
+ st.markdown("<div style='color:#6b6880; margin-bottom:1rem;'>Your XP, level, streak and badges.</div>", unsafe_allow_html=True)
1154
+
1155
+ # XP / Level hero card
1156
+ st.markdown(f"""
1157
+ <div style='background:linear-gradient(135deg,#6c47ff,#a78bfa,#f97316); border-radius:22px; padding:2.5rem 2rem; text-align:center; margin-bottom:1.5rem; box-shadow:0 8px 40px rgba(108,71,255,0.18);'>
1158
+ <div style='font-size:0.78rem; font-weight:700; text-transform:uppercase; letter-spacing:0.08em; color:rgba(255,255,255,0.7); margin-bottom:0.5rem;'>Current Level</div>
1159
+ <div style='font-family:Fraunces,serif; font-size:2.8rem; font-weight:900; color:#fff; margin-bottom:0.2rem;'>{level_name}</div>
1160
+ <div style='color:rgba(255,255,255,0.85); font-size:1.1rem; font-weight:600;'>{xp} XP total</div>
1161
+ {"<div style='color:rgba(255,255,255,0.65); font-size:0.88rem; margin-top:0.3rem;'>" + str(xp_to_next) + " XP to " + str(next_level_name) + "</div>" if next_level_name else "<div style='color:rgba(255,255,255,0.65); font-size:0.88rem; margin-top:0.3rem;'>Maximum level reached! πŸš€</div>"}
1162
+ <div style='background:rgba(255,255,255,0.2); border-radius:99px; height:8px; margin:1rem auto 0; max-width:320px; overflow:hidden;'>
1163
+ <div style='height:100%; width:{level_pct}%; background:#fff; border-radius:99px;'></div>
1164
+ </div>
1165
+ </div>
1166
+ """, unsafe_allow_html=True)
1167
+
1168
+ # Stats row
1169
+ total_quizzes = gami.get("total_quizzes", 0)
1170
+ badges_count = len(earned)
1171
+ s1, s2, s3, s4 = st.columns(4)
1172
+ for col, icon, val, label in [
1173
+ (s1, "πŸ”₯", f"{streak} days", "Current Streak"),
1174
+ (s2, "⚑", f"{xp} XP", "Total XP"),
1175
+ (s3, "🧩", str(total_quizzes), "Quizzes Done"),
1176
+ (s4, "πŸ…", f"{badges_count}/{len(BADGES)}", "Badges Earned"),
1177
+ ]:
1178
+ with col:
1179
+ st.markdown(f"""
1180
+ <div style='background:#fff; border:1.5px solid #e2ddf5; border-radius:16px; padding:1.2rem 0.8rem; text-align:center; box-shadow:0 2px 10px rgba(108,71,255,0.07);'>
1181
+ <div style='font-size:1.6rem; margin-bottom:0.3rem;'>{icon}</div>
1182
+ <div style='font-family:Fraunces,serif; font-size:1.4rem; font-weight:700; color:#1a1523;'>{val}</div>
1183
+ <div style='font-size:0.75rem; color:#6b6880; font-weight:600;'>{label}</div>
1184
+ </div>
1185
+ """, unsafe_allow_html=True)
1186
+
1187
+ st.markdown("---")
1188
+ st.markdown("### πŸ… Badge Collection")
1189
+
1190
+ # Filter tabs
1191
+ filter_tab1, filter_tab2 = st.tabs(["All Badges", "Earned Only"])
1192
+
1193
+ def render_badges(badge_list):
1194
+ cols = st.columns(4)
1195
+ for i, (key, badge) in enumerate(badge_list):
1196
+ is_earned = key in earned
1197
+ card_class = "badge-card earned" if is_earned else "badge-card locked"
1198
+ lock_icon = badge["icon"] if is_earned else "πŸ”’"
1199
+ opacity = "1" if is_earned else "0.5"
1200
+ with cols[i % 4]:
1201
+ st.markdown(f"""
1202
+ <div class='{card_class}' style='opacity:{opacity};'>
1203
+ <div style='font-size:2rem; margin-bottom:0.4rem;'>{lock_icon}</div>
1204
+ <div style='font-weight:700; font-size:0.88rem; color:#1a1523; margin-bottom:0.2rem;'>{badge["name"]}</div>
1205
+ <div style='font-size:0.76rem; color:#6b6880; line-height:1.4;'>{badge["desc"]}</div>
1206
+ {"<div style='margin-top:0.4rem; font-size:0.7rem; font-weight:700; color:#6c47ff; text-transform:uppercase; letter-spacing:0.05em;'>βœ“ Earned</div>" if is_earned else ""}
1207
+ </div>
1208
+ """, unsafe_allow_html=True)
1209
+
1210
+ with filter_tab1:
1211
+ render_badges(list(BADGES.items()))
1212
+ with filter_tab2:
1213
+ earned_list = [(k, v) for k, v in BADGES.items() if k in earned]
1214
+ if earned_list:
1215
+ render_badges(earned_list)
1216
+ else:
1217
+ st.markdown("""
1218
+ <div style='text-align:center; padding:3rem; color:#6b6880;'>
1219
+ <div style='font-size:2.5rem; margin-bottom:0.75rem;'>πŸ”’</div>
1220
+ <div>No badges yet β€” complete quizzes and study sessions to earn them!</div>
1221
+ </div>
1222
+ """, unsafe_allow_html=True)
1223
+
1224
+ # How to earn XP guide
1225
+ st.markdown("---")
1226
+ st.markdown("### ⚑ How to Earn XP")
1227
+ xp_guide = [
1228
+ ("πŸ“š", "Study session", "+10 XP"),
1229
+ ("πŸƒ", "Flashcard deck", "+10 XP"),
1230
+ ("🧩", "Complete a quiz", "+20 XP"),
1231
+ ("🎯", "Score β‰₯ 60%", "+15 XP"),
1232
+ ("πŸŽ‰", "Score β‰₯ 80%", "+30 XP"),
1233
+ ("πŸ’―", "Perfect score 100%","+50 XP"),
1234
+ ]
1235
+ xp_cols = st.columns(3)
1236
+ for i, (icon, action, xp_val) in enumerate(xp_guide):
1237
+ with xp_cols[i % 3]:
1238
+ st.markdown(f"""
1239
+ <div style='background:#fff; border:1px solid #e2ddf5; border-radius:12px; padding:0.8rem 1rem; margin:0.3rem 0; display:flex; justify-content:space-between; align-items:center; box-shadow:0 1px 6px rgba(108,71,255,0.05);'>
1240
+ <div style='font-size:0.9rem; color:#1a1523;'>{icon} {action}</div>
1241
+ <div style='font-weight:700; color:#6c47ff; font-size:0.9rem;'>{xp_val}</div>
1242
+ </div>
1243
+ """, unsafe_allow_html=True)
content_generator.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from groq import Groq
3
+
4
+ STYLE_TIMES = {
5
+ "Summary Notes": "3-5 min",
6
+ "Detailed Explanation": "8-12 min",
7
+ "Bullet Points": "2-4 min",
8
+ "Concept Map": "5-7 min"
9
+ }
10
+
11
+ GROQ_API_KEY = "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"
12
+
13
+
14
+ def generate_content(topic, level, content_style, focus=""):
15
+ focus_note = " Focus specifically on: " + focus + "." if focus else ""
16
+ prompt = (
17
+ "Generate structured study notes for '" + topic + "' at " + level + " level "
18
+ "in the style of " + content_style + "." + focus_note + "\n\n"
19
+ "Return ONLY a JSON object with:\n"
20
+ "- sections: array of 3 objects each with 'title' and 'content'\n"
21
+ "- key_terms: array of 4-6 objects each with 'term' and 'definition'\n"
22
+ "- summary: one string (1-2 sentences)\n\n"
23
+ "JSON only, no markdown, no backticks, no extra text."
24
+ )
25
+
26
+ try:
27
+ client = Groq(api_key=GROQ_API_KEY)
28
+ r = client.chat.completions.create(
29
+ model="llama-3.3-70b-versatile",
30
+ messages=[{"role": "user", "content": prompt}],
31
+ max_tokens=1500
32
+ )
33
+ raw = r.choices[0].message.content.strip()
34
+ # Strip markdown fences if model adds them anyway
35
+ if raw.startswith("```"):
36
+ raw = raw.split("```", 2)[1]
37
+ if raw.startswith("json"):
38
+ raw = raw[4:]
39
+ data = json.loads(raw.strip())
40
+ return {
41
+ "topic": topic.title(),
42
+ "level": level,
43
+ "style": content_style,
44
+ "read_time": STYLE_TIMES.get(content_style, "5 min"),
45
+ "sections": data["sections"],
46
+ "key_terms": data["key_terms"],
47
+ "summary": data["summary"],
48
+ "ai_generated": True,
49
+ }
50
+ except Exception as e:
51
+ # Graceful fallback so the app never crashes
52
+ return {
53
+ "topic": topic.title(),
54
+ "level": level,
55
+ "style": content_style,
56
+ "read_time": STYLE_TIMES.get(content_style, "5 min"),
57
+ "sections": [
58
+ {"title": "Overview", "content": f"Study notes for {topic} at {level} level could not be generated. Please try again."},
59
+ ],
60
+ "key_terms": [],
61
+ "summary": f"Content generation failed: {str(e)}",
62
+ "ai_generated": False,
63
+ }
evaluation.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ evaluation.py
3
+ Evaluates quiz answers and returns detailed results.
4
+ Supports Multiple Choice, True/False, Short Answer, Fill in the Blank.
5
+ """
6
+
7
+
8
+ def evaluate_answers(quiz: dict, answers: dict) -> dict:
9
+ questions = quiz["questions"]
10
+ total = len(questions)
11
+ correct_count = 0
12
+ details = []
13
+
14
+ for i, q in enumerate(questions):
15
+ user_ans = str(answers.get(i, "")).strip().lower()
16
+ correct_ans = str(q.get("answer", "")).strip().lower()
17
+ q_type = q.get("type", "")
18
+
19
+ if q_type in ("Short Answer", "Fill in the Blank"):
20
+ # Accept if either string contains the other
21
+ is_correct = correct_ans in user_ans or user_ans in correct_ans
22
+ else:
23
+ is_correct = user_ans == correct_ans
24
+
25
+ if is_correct:
26
+ correct_count += 1
27
+
28
+ details.append({
29
+ "correct": is_correct,
30
+ "correct_answer": q.get("answer", "N/A"),
31
+ "explanation": q.get("explanation", ""),
32
+ })
33
+
34
+ score_pct = round((correct_count / total) * 100) if total > 0 else 0
35
+
36
+ if score_pct == 100:
37
+ feedback = "🌟 Perfect score! Outstanding work!"
38
+ elif score_pct >= 80:
39
+ feedback = "πŸŽ‰ Excellent! You have a strong grasp of the material."
40
+ elif score_pct >= 60:
41
+ feedback = "πŸ‘ Good effort! Review the questions you missed to reinforce your understanding."
42
+ elif score_pct >= 40:
43
+ feedback = "πŸ“– Keep practising. Revisit the study material for the topics you found tricky."
44
+ else:
45
+ feedback = "πŸ’ͺ Don't give up! Go back to the study notes and try again β€” you'll improve."
46
+
47
+ return {
48
+ "score_percent": score_pct,
49
+ "correct": correct_count,
50
+ "total": total,
51
+ "feedback": feedback,
52
+ "details": details,
53
+ }
flashcard_generator.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ flashcard_generator.py
3
+ Generates flashcard sets from a topic using Groq LLM.
4
+ """
5
+ import json
6
+ from groq import Groq
7
+
8
+ GROQ_API_KEY = "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"
9
+
10
+
11
+ def generate_flashcards(topic: str, level: str, num_cards: int = 10) -> list:
12
+ """Return a list of {front, back} flashcard dicts."""
13
+ prompt = (
14
+ f"Generate {num_cards} flashcards for '{topic}' at {level} level.\n"
15
+ "Return ONLY a JSON object with key 'cards' (array).\n"
16
+ "Each card needs:\n"
17
+ " - front: a short question or term (max 15 words)\n"
18
+ " - back: the answer or definition (max 40 words)\n"
19
+ "JSON only, no markdown, no backticks, no extra text."
20
+ )
21
+ try:
22
+ client = Groq(api_key=GROQ_API_KEY)
23
+ r = client.chat.completions.create(
24
+ model="llama-3.3-70b-versatile",
25
+ messages=[{"role": "user", "content": prompt}],
26
+ max_tokens=1500,
27
+ )
28
+ raw = r.choices[0].message.content.strip()
29
+ if raw.startswith("```"):
30
+ raw = raw.split("```", 2)[1]
31
+ if raw.startswith("json"):
32
+ raw = raw[4:]
33
+ data = json.loads(raw.strip())
34
+ return data.get("cards", [])
35
+ except Exception as e:
36
+ return [{"front": "Generation failed", "back": str(e)}]
gamification.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ gamification.py
3
+ Handles XP, streaks, badges, and levels for LearnCraft.
4
+ """
5
+ import json
6
+ import os
7
+ from datetime import date, timedelta
8
+
9
+ GAMIFICATION_FILE = "gamification.json"
10
+
11
+ # ── XP values ────────────────────────────────────────────────────────────────
12
+ XP_STUDY_SESSION = 10
13
+ XP_QUIZ_COMPLETE = 20
14
+ XP_PERFECT_SCORE = 50
15
+ XP_SCORE_ABOVE_80 = 30
16
+ XP_SCORE_ABOVE_60 = 15
17
+ XP_FLASHCARD_DECK = 10
18
+ XP_STREAK_BONUS = 5 # per day of streak
19
+
20
+ # ── Level thresholds ─────────────────────────────────────────────────────────
21
+ LEVELS = [
22
+ (0, "🌱 Seedling"),
23
+ (50, "πŸ“– Reader"),
24
+ (150, "πŸŽ“ Student"),
25
+ (300, "πŸ”¬ Scholar"),
26
+ (500, "πŸ† Expert"),
27
+ (800, "🌟 Master"),
28
+ (1200, "πŸš€ Genius"),
29
+ ]
30
+
31
+ # ── Badge definitions ─────────────────────────────────────────────────────────
32
+ BADGES = {
33
+ "first_quiz": {"name": "First Quiz", "icon": "🎯", "desc": "Complete your first quiz"},
34
+ "perfect_score": {"name": "Perfect Score", "icon": "πŸ’―", "desc": "Score 100% on a quiz"},
35
+ "streak_3": {"name": "3-Day Streak", "icon": "πŸ”₯", "desc": "Study 3 days in a row"},
36
+ "streak_7": {"name": "Week Warrior", "icon": "⚑", "desc": "Study 7 days in a row"},
37
+ "streak_14": {"name": "Fortnight Hero", "icon": "πŸ—“οΈ", "desc": "Study 14 days in a row"},
38
+ "topics_5": {"name": "Explorer", "icon": "πŸ—ΊοΈ", "desc": "Study 5 different topics"},
39
+ "topics_10": {"name": "Polymath", "icon": "🧠", "desc": "Study 10 different topics"},
40
+ "quizzes_10": {"name": "Quiz Master", "icon": "🧩", "desc": "Complete 10 quizzes"},
41
+ "quizzes_25": {"name": "Quiz Champion", "icon": "πŸ…", "desc": "Complete 25 quizzes"},
42
+ "score_above_80": {"name": "High Achiever", "icon": "πŸŽ‰", "desc": "Score above 80% on a quiz"},
43
+ "flashcards": {"name": "Card Shark", "icon": "πŸƒ", "desc": "Complete a flashcard deck"},
44
+ "level_scholar": {"name": "Scholar", "icon": "πŸ”¬", "desc": "Reach Scholar level (300 XP)"},
45
+ "level_expert": {"name": "Expert", "icon": "πŸ†", "desc": "Reach Expert level (500 XP)"},
46
+ "level_master": {"name": "Master", "icon": "🌟", "desc": "Reach Master level (800 XP)"},
47
+ "notes_saver": {"name": "Note Taker", "icon": "πŸ“", "desc": "Save your first note"},
48
+ }
49
+
50
+
51
+ def load_gamification() -> dict:
52
+ if os.path.exists(GAMIFICATION_FILE):
53
+ try:
54
+ with open(GAMIFICATION_FILE, "r") as f:
55
+ return json.load(f)
56
+ except (json.JSONDecodeError, IOError):
57
+ pass
58
+ return {
59
+ "xp": 0,
60
+ "badges": [],
61
+ "streak": 0,
62
+ "last_study_date": None,
63
+ "total_quizzes": 0,
64
+ "study_dates": [],
65
+ }
66
+
67
+
68
+ def save_gamification(data: dict) -> None:
69
+ try:
70
+ with open(GAMIFICATION_FILE, "w") as f:
71
+ json.dump(data, f, indent=2)
72
+ except IOError:
73
+ pass
74
+
75
+
76
+ def get_level(xp: int) -> tuple:
77
+ """Return (level_name, xp_for_next, xp_in_current_level, progress_pct)."""
78
+ current_level = LEVELS[0]
79
+ next_level = None
80
+ for i, (threshold, name) in enumerate(LEVELS):
81
+ if xp >= threshold:
82
+ current_level = (threshold, name)
83
+ next_level = LEVELS[i + 1] if i + 1 < len(LEVELS) else None
84
+ if next_level:
85
+ xp_start = current_level[0]
86
+ xp_end = next_level[0]
87
+ progress = (xp - xp_start) / (xp_end - xp_start)
88
+ return current_level[1], next_level[1], xp_end - xp, round(progress * 100)
89
+ return current_level[1], None, 0, 100
90
+
91
+
92
+ def update_streak(data: dict) -> dict:
93
+ today = str(date.today())
94
+ yesterday = str(date.today() - timedelta(days=1))
95
+ last = data.get("last_study_date")
96
+
97
+ study_dates = data.get("study_dates", [])
98
+ if today not in study_dates:
99
+ study_dates.append(today)
100
+ data["study_dates"] = study_dates
101
+
102
+ if last == today:
103
+ pass # already counted today
104
+ elif last == yesterday:
105
+ data["streak"] = data.get("streak", 0) + 1
106
+ data["last_study_date"] = today
107
+ else:
108
+ data["streak"] = 1
109
+ data["last_study_date"] = today
110
+
111
+ return data
112
+
113
+
114
+ def award_xp(data: dict, amount: int, reason: str = "") -> tuple:
115
+ """Add XP and return (new_data, xp_awarded, level_up_msg)."""
116
+ old_xp = data.get("xp", 0)
117
+ old_level = get_level(old_xp)[0]
118
+ data["xp"] = old_xp + amount
119
+ new_level = get_level(data["xp"])[0]
120
+ level_up = new_level if new_level != old_level else None
121
+ return data, amount, level_up
122
+
123
+
124
+ def check_and_award_badges(data: dict, context: dict) -> list:
125
+ """
126
+ Check badge conditions and award new ones.
127
+ context keys: score, topics_count, quizzes_count, event
128
+ Returns list of newly awarded badge keys.
129
+ """
130
+ earned = set(data.get("badges", []))
131
+ new_ones = []
132
+
133
+ score = context.get("score", -1)
134
+ topics_count = context.get("topics_count", 0)
135
+ quizzes_count = context.get("quizzes_count", 0)
136
+ event = context.get("event", "")
137
+ streak = data.get("streak", 0)
138
+ xp = data.get("xp", 0)
139
+
140
+ checks = {
141
+ "first_quiz": quizzes_count >= 1,
142
+ "perfect_score": score == 100,
143
+ "streak_3": streak >= 3,
144
+ "streak_7": streak >= 7,
145
+ "streak_14": streak >= 14,
146
+ "topics_5": topics_count >= 5,
147
+ "topics_10": topics_count >= 10,
148
+ "quizzes_10": quizzes_count >= 10,
149
+ "quizzes_25": quizzes_count >= 25,
150
+ "score_above_80": score >= 80,
151
+ "flashcards": event == "flashcards",
152
+ "level_scholar": xp >= 300,
153
+ "level_expert": xp >= 500,
154
+ "level_master": xp >= 800,
155
+ "notes_saver": event == "note_saved",
156
+ }
157
+
158
+ for key, condition in checks.items():
159
+ if condition and key not in earned:
160
+ earned.add(key)
161
+ new_ones.append(key)
162
+
163
+ data["badges"] = list(earned)
164
+ return new_ones
165
+
166
+
167
+ def record_quiz(data: dict, score: int, topics_count: int) -> dict:
168
+ data["total_quizzes"] = data.get("total_quizzes", 0) + 1
169
+ return data
170
+
171
+
172
+ def get_xp_for_quiz(score: int) -> int:
173
+ xp = XP_QUIZ_COMPLETE
174
+ if score == 100:
175
+ xp += XP_PERFECT_SCORE
176
+ elif score >= 80:
177
+ xp += XP_SCORE_ABOVE_80
178
+ elif score >= 60:
179
+ xp += XP_SCORE_ABOVE_60
180
+ return xp
learning_progress.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "topics_studied": [
3
+ "quantum mechanics",
4
+ "Quantum Mechanics",
5
+ "food",
6
+ "ML"
7
+ ],
8
+ "scores": [
9
+ 60,
10
+ 60,
11
+ 60,
12
+ 60,
13
+ 20
14
+ ],
15
+ "best_score": 60,
16
+ "topic_scores": {
17
+ "Quantum Mechanics": 60
18
+ },
19
+ "sessions": [
20
+ {
21
+ "topic": "Quantum Mechanics",
22
+ "score": 60,
23
+ "date": "2026-04-22"
24
+ },
25
+ {
26
+ "topic": "Quantum Mechanics",
27
+ "score": 60,
28
+ "date": "2026-04-22"
29
+ },
30
+ {
31
+ "topic": "Quantum Mechanics",
32
+ "score": 60,
33
+ "date": "2026-04-22"
34
+ },
35
+ {
36
+ "topic": "Quantum Mechanics",
37
+ "score": 60,
38
+ "date": "2026-04-22"
39
+ },
40
+ {
41
+ "topic": "Quantum Mechanics",
42
+ "score": 20,
43
+ "date": "2026-04-22"
44
+ }
45
+ ]
46
+ }
notes.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "topic": "Quantum Mechanics",
4
+ "note": "Quantum mechanics is a branch of physics that studies the behavior of matter and energy at the smallest scales, introducing new concepts such as wave-particle duality and uncertainty. The key principles of quantum mechanics, including superposition and the Heisenberg Uncertainty Principle, form the basis of quantum theory and help to explain the behavior of particles at the atomic and subatomic level.",
5
+ "date": "2026-04-22"
6
+ },
7
+ {
8
+ "topic": "Quantum Mechanics",
9
+ "note": "Quantum mechanics is a fundamental theory that describes the behavior of matter and energy at the smallest scales, and has numerous applications in fields such as chemistry, materials science, and electronics. It is based on principles such as wave-particle duality, uncertainty, and superposition, and has led to new areas of research, including quantum computing and quantum information theory.\n\n",
10
+ "date": "2026-04-22"
11
+ }
12
+ ]
pdf_export.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ pdf_export.py
3
+ Generates formatted PDF exports for study notes and quiz results using reportlab.
4
+ """
5
+ import io
6
+ from datetime import date
7
+ from reportlab.lib.pagesizes import A4
8
+ from reportlab.lib import colors
9
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
10
+ from reportlab.lib.units import mm
11
+ from reportlab.platypus import (
12
+ SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
13
+ HRFlowable, KeepTogether
14
+ )
15
+ from reportlab.lib.enums import TA_CENTER, TA_LEFT
16
+
17
+ # ── Brand colours ─────────────────────────────────────────────────────────────
18
+ PURPLE = colors.HexColor("#6c47ff")
19
+ ORANGE = colors.HexColor("#f97316")
20
+ LIGHT = colors.HexColor("#f7f4ef")
21
+ MUTED = colors.HexColor("#6b6880")
22
+ DARK = colors.HexColor("#1a1523")
23
+ WHITE = colors.white
24
+ GREEN = colors.HexColor("#10b981")
25
+ RED = colors.HexColor("#ef4444")
26
+
27
+
28
+ def _base_styles():
29
+ base = getSampleStyleSheet()
30
+ styles = {
31
+ "title": ParagraphStyle(
32
+ "Title2", parent=base["Title"],
33
+ fontSize=26, textColor=PURPLE, spaceAfter=4,
34
+ fontName="Helvetica-Bold", alignment=TA_CENTER,
35
+ ),
36
+ "subtitle": ParagraphStyle(
37
+ "Subtitle", parent=base["Normal"],
38
+ fontSize=11, textColor=MUTED, spaceAfter=16,
39
+ fontName="Helvetica", alignment=TA_CENTER,
40
+ ),
41
+ "section_head": ParagraphStyle(
42
+ "SectionHead", parent=base["Heading2"],
43
+ fontSize=13, textColor=PURPLE, spaceBefore=14, spaceAfter=6,
44
+ fontName="Helvetica-Bold",
45
+ ),
46
+ "body": ParagraphStyle(
47
+ "Body2", parent=base["Normal"],
48
+ fontSize=10, textColor=DARK, spaceAfter=6,
49
+ fontName="Helvetica", leading=15,
50
+ ),
51
+ "term": ParagraphStyle(
52
+ "Term", parent=base["Normal"],
53
+ fontSize=10, textColor=PURPLE, spaceAfter=2,
54
+ fontName="Helvetica-Bold",
55
+ ),
56
+ "definition": ParagraphStyle(
57
+ "Def", parent=base["Normal"],
58
+ fontSize=9.5, textColor=MUTED, spaceAfter=8,
59
+ fontName="Helvetica", leading=14,
60
+ ),
61
+ "summary_box": ParagraphStyle(
62
+ "SummaryBox", parent=base["Normal"],
63
+ fontSize=10, textColor=DARK, spaceAfter=6,
64
+ fontName="Helvetica-Oblique", leading=15,
65
+ ),
66
+ "label": ParagraphStyle(
67
+ "Label", parent=base["Normal"],
68
+ fontSize=8, textColor=MUTED, spaceAfter=2,
69
+ fontName="Helvetica", alignment=TA_CENTER,
70
+ ),
71
+ "score_big": ParagraphStyle(
72
+ "ScoreBig", parent=base["Normal"],
73
+ fontSize=40, textColor=PURPLE, spaceAfter=4,
74
+ fontName="Helvetica-Bold", alignment=TA_CENTER,
75
+ ),
76
+ "feedback": ParagraphStyle(
77
+ "Feedback", parent=base["Normal"],
78
+ fontSize=12, textColor=DARK, spaceAfter=12,
79
+ fontName="Helvetica-Bold", alignment=TA_CENTER,
80
+ ),
81
+ "q_text": ParagraphStyle(
82
+ "QText", parent=base["Normal"],
83
+ fontSize=10, textColor=DARK, spaceAfter=3,
84
+ fontName="Helvetica-Bold",
85
+ ),
86
+ "q_detail": ParagraphStyle(
87
+ "QDetail", parent=base["Normal"],
88
+ fontSize=9.5, textColor=MUTED, spaceAfter=2,
89
+ fontName="Helvetica",
90
+ ),
91
+ }
92
+ return styles
93
+
94
+
95
+ def export_study_notes_pdf(content: dict) -> bytes:
96
+ """Generate a PDF for study notes. Returns bytes."""
97
+ buffer = io.BytesIO()
98
+ doc = SimpleDocTemplate(
99
+ buffer, pagesize=A4,
100
+ leftMargin=20*mm, rightMargin=20*mm,
101
+ topMargin=18*mm, bottomMargin=18*mm,
102
+ )
103
+ s = _base_styles()
104
+ story = []
105
+
106
+ # Header
107
+ story.append(Paragraph("LearnCraft", s["title"]))
108
+ story.append(Paragraph("Personalized Study Notes", s["subtitle"]))
109
+ story.append(HRFlowable(width="100%", thickness=2, color=PURPLE, spaceAfter=12))
110
+
111
+ # Meta table
112
+ meta_data = [
113
+ ["Topic", content.get("topic", "β€”"), "Level", content.get("level", "β€”")],
114
+ ["Style", content.get("style", "β€”"), "Read Time", content.get("read_time", "β€”")],
115
+ ["Generated", str(date.today()), "", ""],
116
+ ]
117
+ meta_table = Table(meta_data, colWidths=[30*mm, 65*mm, 30*mm, 50*mm])
118
+ meta_table.setStyle(TableStyle([
119
+ ("FONTNAME", (0,0), (-1,-1), "Helvetica"),
120
+ ("FONTSIZE", (0,0), (-1,-1), 9),
121
+ ("FONTNAME", (0,0), (0,-1), "Helvetica-Bold"),
122
+ ("FONTNAME", (2,0), (2,-1), "Helvetica-Bold"),
123
+ ("TEXTCOLOR", (0,0), (0,-1), PURPLE),
124
+ ("TEXTCOLOR", (2,0), (2,-1), PURPLE),
125
+ ("TEXTCOLOR", (1,0), (1,-1), DARK),
126
+ ("TEXTCOLOR", (3,0), (3,-1), DARK),
127
+ ("BACKGROUND", (0,0), (-1,-1), LIGHT),
128
+ ("ROWBACKGROUNDS", (0,0), (-1,-1), [LIGHT, WHITE]),
129
+ ("GRID", (0,0), (-1,-1), 0.5, colors.HexColor("#e2ddf5")),
130
+ ("ROUNDEDCORNERS", [4]),
131
+ ("TOPPADDING", (0,0), (-1,-1), 5),
132
+ ("BOTTOMPADDING",(0,0), (-1,-1), 5),
133
+ ("LEFTPADDING", (0,0), (-1,-1), 8),
134
+ ]))
135
+ story.append(meta_table)
136
+ story.append(Spacer(1, 10))
137
+
138
+ # Sections
139
+ for section in content.get("sections", []):
140
+ story.append(Paragraph(f"β–Έ {section['title']}", s["section_head"]))
141
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#e2ddf5"), spaceAfter=6))
142
+ story.append(Paragraph(section["content"], s["body"]))
143
+
144
+ # Key terms
145
+ key_terms = content.get("key_terms", [])
146
+ if key_terms:
147
+ story.append(Spacer(1, 6))
148
+ story.append(Paragraph("Key Terms", s["section_head"]))
149
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#e2ddf5"), spaceAfter=8))
150
+ for term in key_terms:
151
+ story.append(Paragraph(term["term"], s["term"]))
152
+ story.append(Paragraph(term["definition"], s["definition"]))
153
+
154
+ # Summary
155
+ summary = content.get("summary", "")
156
+ if summary:
157
+ story.append(Spacer(1, 4))
158
+ story.append(Paragraph("Quick Summary", s["section_head"]))
159
+ summary_table = Table([[Paragraph(f'"{summary}"', s["summary_box"])]], colWidths=[170*mm])
160
+ summary_table.setStyle(TableStyle([
161
+ ("BACKGROUND", (0,0), (-1,-1), colors.HexColor("#ede9fe")),
162
+ ("LEFTPADDING", (0,0), (-1,-1), 12),
163
+ ("RIGHTPADDING", (0,0), (-1,-1), 12),
164
+ ("TOPPADDING", (0,0), (-1,-1), 10),
165
+ ("BOTTOMPADDING",(0,0), (-1,-1), 10),
166
+ ("ROUNDEDCORNERS", [8]),
167
+ ]))
168
+ story.append(summary_table)
169
+
170
+ # Footer
171
+ story.append(Spacer(1, 16))
172
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#e2ddf5"), spaceAfter=6))
173
+ story.append(Paragraph(f"Generated by LearnCraft Β· {date.today()}", s["label"]))
174
+
175
+ doc.build(story)
176
+ return buffer.getvalue()
177
+
178
+
179
+ def export_quiz_results_pdf(quiz: dict, results: dict, answers: dict) -> bytes:
180
+ """Generate a PDF for quiz results. Returns bytes."""
181
+ buffer = io.BytesIO()
182
+ doc = SimpleDocTemplate(
183
+ buffer, pagesize=A4,
184
+ leftMargin=20*mm, rightMargin=20*mm,
185
+ topMargin=18*mm, bottomMargin=18*mm,
186
+ )
187
+ s = _base_styles()
188
+ story = []
189
+
190
+ # Header
191
+ story.append(Paragraph("LearnCraft", s["title"]))
192
+ story.append(Paragraph("Quiz Results Report", s["subtitle"]))
193
+ story.append(HRFlowable(width="100%", thickness=2, color=PURPLE, spaceAfter=14))
194
+
195
+ # Score hero
196
+ score = results["score_percent"]
197
+ score_col = GREEN if score >= 60 else RED
198
+ story.append(Paragraph(f"{score}%", ParagraphStyle(
199
+ "BigScore", fontSize=48, textColor=score_col,
200
+ fontName="Helvetica-Bold", alignment=TA_CENTER, spaceAfter=4,
201
+ )))
202
+ story.append(Paragraph(
203
+ f"{results['correct']} / {results['total']} correct Β· {results['feedback']}",
204
+ s["feedback"]
205
+ ))
206
+ story.append(Spacer(1, 6))
207
+
208
+ # Meta
209
+ meta_data = [
210
+ ["Topic", quiz.get("topic", "β€”"), "Difficulty", quiz.get("difficulty", "β€”")],
211
+ ["Questions", str(results["total"]), "Date", str(date.today())],
212
+ ]
213
+ meta_table = Table(meta_data, colWidths=[30*mm, 65*mm, 30*mm, 50*mm])
214
+ meta_table.setStyle(TableStyle([
215
+ ("FONTNAME", (0,0), (-1,-1), "Helvetica"),
216
+ ("FONTSIZE", (0,0), (-1,-1), 9),
217
+ ("FONTNAME", (0,0), (0,-1), "Helvetica-Bold"),
218
+ ("FONTNAME", (2,0), (2,-1), "Helvetica-Bold"),
219
+ ("TEXTCOLOR", (0,0), (0,-1), PURPLE),
220
+ ("TEXTCOLOR", (2,0), (2,-1), PURPLE),
221
+ ("BACKGROUND", (0,0), (-1,-1), LIGHT),
222
+ ("ROWBACKGROUNDS", (0,0), (-1,-1), [LIGHT, WHITE]),
223
+ ("GRID", (0,0), (-1,-1), 0.5, colors.HexColor("#e2ddf5")),
224
+ ("TOPPADDING", (0,0), (-1,-1), 5),
225
+ ("BOTTOMPADDING",(0,0), (-1,-1), 5),
226
+ ("LEFTPADDING", (0,0), (-1,-1), 8),
227
+ ]))
228
+ story.append(meta_table)
229
+ story.append(Spacer(1, 14))
230
+
231
+ # Answer review
232
+ story.append(Paragraph("Answer Review", s["section_head"]))
233
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#e2ddf5"), spaceAfter=8))
234
+
235
+ for i, q in enumerate(quiz.get("questions", [])):
236
+ correct = results["details"][i]["correct"]
237
+ user_ans = str(answers.get(i, "No answer"))
238
+ right_ans = results["details"][i]["correct_answer"]
239
+ explanation= results["details"][i].get("explanation", "")
240
+ icon = "βœ“" if correct else "βœ—"
241
+ bg_color = colors.HexColor("#d1fae5") if correct else colors.HexColor("#fee2e2")
242
+ icon_color = GREEN if correct else RED
243
+
244
+ block = [
245
+ [
246
+ Paragraph(f"<font color='{'#10b981' if correct else '#ef4444'}'><b>{icon}</b></font> Q{i+1}: {q['question']}", s["q_text"]),
247
+ ],
248
+ [
249
+ Paragraph(
250
+ f"Your answer: <font color='{'#10b981' if correct else '#ef4444'}'><b>{user_ans}</b></font> | "
251
+ f"Correct: <font color='#10b981'><b>{right_ans}</b></font>"
252
+ + (f"<br/><i>{explanation}</i>" if explanation else ""),
253
+ s["q_detail"]
254
+ ),
255
+ ],
256
+ ]
257
+ t = Table(block, colWidths=[170*mm])
258
+ t.setStyle(TableStyle([
259
+ ("BACKGROUND", (0,0), (-1,-1), bg_color),
260
+ ("LEFTPADDING", (0,0), (-1,-1), 10),
261
+ ("RIGHTPADDING", (0,0), (-1,-1), 10),
262
+ ("TOPPADDING", (0,0), (-1,-1), 8),
263
+ ("BOTTOMPADDING", (0,0), (-1,-1), 8),
264
+ ("ROUNDEDCORNERS", [6]),
265
+ ]))
266
+ story.append(KeepTogether([t, Spacer(1, 5)]))
267
+
268
+ # Footer
269
+ story.append(Spacer(1, 12))
270
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#e2ddf5"), spaceAfter=6))
271
+ story.append(Paragraph(f"Generated by LearnCraft Β· {date.today()}", s["label"]))
272
+
273
+ doc.build(story)
274
+ return buffer.getvalue()
quiz_generator.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from groq import Groq
3
+
4
+ GROQ_API_KEY = "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"
5
+
6
+
7
+ def generate_quiz(topic, level, num_questions, q_type):
8
+ if q_type != "Mixed":
9
+ type_instruction = "All questions must be type: " + q_type + "."
10
+ else:
11
+ type_instruction = "Mix these types: Multiple Choice, True/False, Fill in the Blank, Short Answer."
12
+
13
+ prompt = (
14
+ "Generate " + str(num_questions) + " quiz questions about '" + topic + "' at " + level + " level.\n"
15
+ + type_instruction + "\n\n"
16
+ "Return ONLY a JSON object with key 'questions' (array).\n"
17
+ "Each question needs:\n"
18
+ " - type: one of 'Multiple Choice', 'True/False', 'Fill in the Blank', 'Short Answer'\n"
19
+ " - question: the question text (for Fill in the Blank, use ___ for the blank)\n"
20
+ " - options: 4 strings for MC, ['True','False'] for TF, [] for others\n"
21
+ " - answer: the correct answer string\n"
22
+ " - explanation: one sentence explaining why\n\n"
23
+ "JSON only, no markdown, no backticks, no extra text."
24
+ )
25
+
26
+ try:
27
+ client = Groq(api_key=GROQ_API_KEY)
28
+ r = client.chat.completions.create(
29
+ model="llama-3.3-70b-versatile",
30
+ messages=[{"role": "user", "content": prompt}],
31
+ max_tokens=1500
32
+ )
33
+ raw = r.choices[0].message.content.strip()
34
+ if raw.startswith("```"):
35
+ raw = raw.split("```", 2)[1]
36
+ if raw.startswith("json"):
37
+ raw = raw[4:]
38
+ data = json.loads(raw.strip())
39
+ return {
40
+ "title": topic.title() + " Quiz",
41
+ "topic": topic.title(),
42
+ "difficulty": level,
43
+ "questions": data["questions"]
44
+ }
45
+ except Exception as e:
46
+ # Fallback single question so app never crashes
47
+ return {
48
+ "title": topic.title() + " Quiz",
49
+ "topic": topic.title(),
50
+ "difficulty": level,
51
+ "questions": [
52
+ {
53
+ "type": "Short Answer",
54
+ "question": f"Quiz generation failed ({str(e)}). Please retry.",
55
+ "options": [],
56
+ "answer": "retry",
57
+ "explanation": "An error occurred while generating the quiz."
58
+ }
59
+ ]
60
+ }
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ streamlit>=1.32.0
2
+ pandas>=2.0.0
3
+ groq>=0.4.0
4
+ reportlab>=4.0.0
5
+ plotly>=5.18.0
tutor.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ tutor.py
3
+ AI Tutor chat powered by Groq. Maintains conversation history per session.
4
+ """
5
+ from groq import Groq
6
+
7
+ GROQ_API_KEY = "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"
8
+
9
+ SYSTEM_PROMPT = """You are LearnCraft Tutor β€” a friendly, encouraging, and highly knowledgeable AI tutor.
10
+ Your role is to help students understand topics deeply, answer their questions clearly, and guide them
11
+ step-by-step when they are confused.
12
+
13
+ Guidelines:
14
+ - Be concise but thorough. Use examples and analogies generously.
15
+ - If a student seems confused, break things down into smaller steps.
16
+ - Celebrate correct answers and effort warmly.
17
+ - Never just give answers to homework β€” guide the student to figure it out.
18
+ - Format responses with clear structure (use short paragraphs, numbered steps where helpful).
19
+ - Keep responses under 200 words unless a complex explanation is needed.
20
+ - Always end with a follow-up question or encouragement to keep the student engaged.
21
+ """
22
+
23
+
24
+ def get_tutor_reply(messages: list, topic: str = "") -> str:
25
+ """
26
+ Send conversation history to Groq and return the tutor's reply.
27
+ messages: list of {"role": "user"/"assistant", "content": str}
28
+ """
29
+ system = SYSTEM_PROMPT
30
+ if topic:
31
+ system += f"\n\nThe student is currently studying: {topic}. Focus your answers around this topic when relevant."
32
+
33
+ try:
34
+ client = Groq(api_key=GROQ_API_KEY)
35
+ response = client.chat.completions.create(
36
+ model="llama-3.3-70b-versatile",
37
+ messages=[{"role": "system", "content": system}] + messages,
38
+ max_tokens=400,
39
+ temperature=0.7,
40
+ )
41
+ return response.choices[0].message.content.strip()
42
+ except Exception as e:
43
+ return f"Sorry, I couldn't connect right now. Please try again! (Error: {str(e)})"
utils.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ utils.py
3
+ Helper utilities: progress persistence, notes, topic lists, learning paths, etc.
4
+ """
5
+ import json
6
+ import os
7
+ from datetime import date
8
+
9
+ PROGRESS_FILE = "learning_progress.json"
10
+ NOTES_FILE = "notes.json"
11
+
12
+ TOPICS = [
13
+ "Photosynthesis",
14
+ "Machine Learning",
15
+ "World War II",
16
+ "Python Functions",
17
+ "Calculus",
18
+ "Climate Change",
19
+ "The French Revolution",
20
+ "DNA Replication",
21
+ "Object-Oriented Programming",
22
+ "The Solar System",
23
+ "Economics Supply & Demand",
24
+ "Quantum Mechanics",
25
+ ]
26
+
27
+ LEARNING_PATHS = {
28
+ "machine learning": ["Python Functions", "Calculus", "Economics Supply & Demand", "Machine Learning"],
29
+ "quantum mechanics": ["Calculus", "The Solar System", "Quantum Mechanics"],
30
+ "dna replication": ["Photosynthesis", "DNA Replication"],
31
+ "calculus": ["Python Functions", "Calculus"],
32
+ "object-oriented programming": ["Python Functions", "Object-Oriented Programming"],
33
+ "climate change": ["Photosynthesis", "Economics Supply & Demand", "Climate Change"],
34
+ }
35
+
36
+
37
+ def get_topics() -> list:
38
+ return sorted(TOPICS)
39
+
40
+
41
+ def load_progress() -> dict:
42
+ """Load progress from JSON file, or return empty structure."""
43
+ if os.path.exists(PROGRESS_FILE):
44
+ try:
45
+ with open(PROGRESS_FILE, "r") as f:
46
+ return json.load(f)
47
+ except (json.JSONDecodeError, IOError):
48
+ pass
49
+ return {
50
+ "topics_studied": [],
51
+ "scores": [],
52
+ "best_score": 0,
53
+ "topic_scores": {}, # {topic: best_score_int}
54
+ "sessions": [], # [{topic, score, date}]
55
+ }
56
+
57
+
58
+ def save_progress(progress: dict, topic: str = None, score: int = None) -> None:
59
+ """Update and persist progress data."""
60
+ if "topic_scores" not in progress:
61
+ progress["topic_scores"] = {}
62
+ if "sessions" not in progress:
63
+ progress["sessions"] = []
64
+
65
+ if topic:
66
+ studied = progress.get("topics_studied", [])
67
+ if topic not in studied:
68
+ studied.append(topic)
69
+ progress["topics_studied"] = studied
70
+
71
+ if score is not None:
72
+ scores = progress.get("scores", [])
73
+ scores.append(score)
74
+ progress["scores"] = scores
75
+ if score > progress.get("best_score", 0):
76
+ progress["best_score"] = score
77
+
78
+ # Per-topic best score (store single int, not list)
79
+ if topic:
80
+ ts = progress["topic_scores"]
81
+ existing = ts.get(topic, 0)
82
+ # Handle legacy list format
83
+ if isinstance(existing, list):
84
+ existing = max(existing) if existing else 0
85
+ ts[topic] = max(existing, score)
86
+ progress["topic_scores"] = ts
87
+
88
+ # Session log
89
+ sessions = progress.get("sessions", [])
90
+ sessions.append({
91
+ "topic": topic or "Unknown",
92
+ "score": score,
93
+ "date": str(date.today())
94
+ })
95
+ progress["sessions"] = sessions
96
+
97
+ try:
98
+ with open(PROGRESS_FILE, "w") as f:
99
+ json.dump(progress, f, indent=2)
100
+ except IOError:
101
+ pass
102
+
103
+
104
+ def get_weak_topics(progress: dict) -> list:
105
+ """Return topics where the best score is below 60%."""
106
+ result = []
107
+ for topic, score in progress.get("topic_scores", {}).items():
108
+ # Handle legacy list format
109
+ if isinstance(score, list):
110
+ score = max(score) if score else 0
111
+ if score < 60:
112
+ result.append(topic)
113
+ return result
114
+
115
+
116
+ def get_learning_path(topic: str) -> list:
117
+ """Return recommended learning path for a topic, or empty list."""
118
+ return LEARNING_PATHS.get(topic.lower().strip(), [])
119
+
120
+
121
+ # ── Notes ────────────────────────────────────────────────────────────────────
122
+
123
+ def load_notes() -> list:
124
+ """Load saved notes from JSON file."""
125
+ if os.path.exists(NOTES_FILE):
126
+ try:
127
+ with open(NOTES_FILE, "r") as f:
128
+ return json.load(f)
129
+ except (json.JSONDecodeError, IOError):
130
+ pass
131
+ return []
132
+
133
+
134
+ def save_note(note_text: str, topic: str) -> None:
135
+ """Append a note and persist to file."""
136
+ notes = load_notes()
137
+ notes.append({
138
+ "topic": topic,
139
+ "note": note_text,
140
+ "date": str(date.today())
141
+ })
142
+ try:
143
+ with open(NOTES_FILE, "w") as f:
144
+ json.dump(notes, f, indent=2)
145
+ except IOError:
146
+ pass
147
+
148
+
149
+ def delete_note(index: int) -> None:
150
+ """Delete a note by its index."""
151
+ notes = load_notes()
152
+ if 0 <= index < len(notes):
153
+ notes.pop(index)
154
+ try:
155
+ with open(NOTES_FILE, "w") as f:
156
+ json.dump(notes, f, indent=2)
157
+ except IOError:
158
+ pass