@gonzih/exam-prep-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,268 @@
1
+ import { getTopicStats, getRecentSessions, TopicStats } from './db.js';
2
+ import { EXAM_TOPICS } from './study-plan.js';
3
+ import { QUESTION_BANK } from './questions.js';
4
+
5
+ export interface ReadinessReport {
6
+ score: number; // 0-100
7
+ strongTopics: TopicScore[];
8
+ weakTopics: TopicScore[];
9
+ coveragePercent: number;
10
+ trend: 'improving' | 'declining' | 'stable';
11
+ recommendation: string;
12
+ estimatedGrade: string;
13
+ }
14
+
15
+ export interface TopicScore {
16
+ topic: string;
17
+ accuracy: number; // 0-100
18
+ attempts: number;
19
+ }
20
+
21
+ // Grade mapping for different exam types
22
+ const GRADE_MAPPINGS: Record<string, (score: number) => string> = {
23
+ AP_Biology: (s) => {
24
+ if (s >= 85) return 'AP Score: 5/5 (Extremely Well Qualified)';
25
+ if (s >= 70) return 'AP Score: 4/5 (Well Qualified)';
26
+ if (s >= 55) return 'AP Score: 3/5 (Qualified)';
27
+ if (s >= 40) return 'AP Score: 2/5 (Possibly Qualified)';
28
+ return 'AP Score: 1/5 (No Recommendation)';
29
+ },
30
+ AP_Chemistry: (s) => {
31
+ if (s >= 85) return 'AP Score: 5/5 (Extremely Well Qualified)';
32
+ if (s >= 70) return 'AP Score: 4/5 (Well Qualified)';
33
+ if (s >= 55) return 'AP Score: 3/5 (Qualified)';
34
+ if (s >= 40) return 'AP Score: 2/5 (Possibly Qualified)';
35
+ return 'AP Score: 1/5 (No Recommendation)';
36
+ },
37
+ AP_US_History: (s) => {
38
+ if (s >= 85) return 'AP Score: 5/5 (Extremely Well Qualified)';
39
+ if (s >= 70) return 'AP Score: 4/5 (Well Qualified)';
40
+ if (s >= 55) return 'AP Score: 3/5 (Qualified)';
41
+ if (s >= 40) return 'AP Score: 2/5 (Possibly Qualified)';
42
+ return 'AP Score: 1/5 (No Recommendation)';
43
+ },
44
+ AP_World_History: (s) => {
45
+ if (s >= 85) return 'AP Score: 5/5 (Extremely Well Qualified)';
46
+ if (s >= 70) return 'AP Score: 4/5 (Well Qualified)';
47
+ if (s >= 55) return 'AP Score: 3/5 (Qualified)';
48
+ if (s >= 40) return 'AP Score: 2/5 (Possibly Qualified)';
49
+ return 'AP Score: 1/5 (No Recommendation)';
50
+ },
51
+ AP_Physics: (s) => {
52
+ if (s >= 85) return 'AP Score: 5/5 (Extremely Well Qualified)';
53
+ if (s >= 70) return 'AP Score: 4/5 (Well Qualified)';
54
+ if (s >= 55) return 'AP Score: 3/5 (Qualified)';
55
+ if (s >= 40) return 'AP Score: 2/5 (Possibly Qualified)';
56
+ return 'AP Score: 1/5 (No Recommendation)';
57
+ },
58
+ AP_Calculus: (s) => {
59
+ if (s >= 85) return 'AP Score: 5/5 (Extremely Well Qualified)';
60
+ if (s >= 70) return 'AP Score: 4/5 (Well Qualified)';
61
+ if (s >= 55) return 'AP Score: 3/5 (Qualified)';
62
+ if (s >= 40) return 'AP Score: 2/5 (Possibly Qualified)';
63
+ return 'AP Score: 1/5 (No Recommendation)';
64
+ },
65
+ SAT_Math: (s) => {
66
+ if (s >= 90) return 'SAT Math Score: 750-800';
67
+ if (s >= 80) return 'SAT Math Score: 680-750';
68
+ if (s >= 70) return 'SAT Math Score: 600-680';
69
+ if (s >= 60) return 'SAT Math Score: 520-600';
70
+ if (s >= 50) return 'SAT Math Score: 440-520';
71
+ return 'SAT Math Score: Below 440';
72
+ },
73
+ SAT_Reading: (s) => {
74
+ if (s >= 90) return 'SAT Reading Score: 35-40';
75
+ if (s >= 80) return 'SAT Reading Score: 30-35';
76
+ if (s >= 70) return 'SAT Reading Score: 25-30';
77
+ if (s >= 60) return 'SAT Reading Score: 20-25';
78
+ return 'SAT Reading Score: Below 20';
79
+ },
80
+ MCAT: (s) => {
81
+ if (s >= 90) return 'MCAT Score: 517-528 (97th+ percentile)';
82
+ if (s >= 80) return 'MCAT Score: 511-516 (80th-96th percentile)';
83
+ if (s >= 70) return 'MCAT Score: 505-510 (60th-79th percentile)';
84
+ if (s >= 60) return 'MCAT Score: 499-504 (40th-59th percentile)';
85
+ return 'MCAT Score: Below 499';
86
+ },
87
+ LSAT: (s) => {
88
+ if (s >= 90) return 'LSAT Score: 170-180 (97th+ percentile)';
89
+ if (s >= 80) return 'LSAT Score: 163-169 (88th-96th percentile)';
90
+ if (s >= 70) return 'LSAT Score: 156-162 (70th-87th percentile)';
91
+ if (s >= 60) return 'LSAT Score: 149-155 (42nd-69th percentile)';
92
+ return 'LSAT Score: Below 149';
93
+ },
94
+ GRE: (s) => {
95
+ if (s >= 90) return 'GRE Score: 165-170 Verbal, 165-170 Quant';
96
+ if (s >= 80) return 'GRE Score: 158-164 Verbal, 158-164 Quant';
97
+ if (s >= 70) return 'GRE Score: 152-157 Verbal, 152-157 Quant';
98
+ if (s >= 60) return 'GRE Score: 145-151 Verbal, 145-151 Quant';
99
+ return 'GRE Score: Below 145';
100
+ },
101
+ };
102
+
103
+ function getGradeEstimate(examType: string, score: number): string {
104
+ const mapper = GRADE_MAPPINGS[examType];
105
+ if (mapper) return mapper(score);
106
+ // Generic AP mapping for unmapped types
107
+ if (examType.startsWith('AP_')) {
108
+ if (score >= 85) return 'AP Score: 5/5 (Extremely Well Qualified)';
109
+ if (score >= 70) return 'AP Score: 4/5 (Well Qualified)';
110
+ if (score >= 55) return 'AP Score: 3/5 (Qualified)';
111
+ if (score >= 40) return 'AP Score: 2/5 (Possibly Qualified)';
112
+ return 'AP Score: 1/5 (No Recommendation)';
113
+ }
114
+ return `Estimated score: ${Math.round(score)}%`;
115
+ }
116
+
117
+ function generateRecommendation(
118
+ score: number,
119
+ weakTopics: TopicScore[],
120
+ coveragePercent: number,
121
+ trend: string,
122
+ daysRemaining: number
123
+ ): string {
124
+ const parts: string[] = [];
125
+
126
+ if (score >= 85) {
127
+ parts.push('Excellent preparation! You are performing at a high level.');
128
+ } else if (score >= 70) {
129
+ parts.push('Good progress! You have a solid foundation.');
130
+ } else if (score >= 55) {
131
+ parts.push('Moderate preparation. More focused study will improve your score.');
132
+ } else {
133
+ parts.push('More intensive study is needed. Focus on building fundamentals.');
134
+ }
135
+
136
+ if (weakTopics.length > 0) {
137
+ const topWeak = weakTopics.slice(0, 3).map(t => t.topic).join(', ');
138
+ parts.push(`Prioritize these weak areas: ${topWeak}.`);
139
+ }
140
+
141
+ if (coveragePercent < 60) {
142
+ parts.push(`You have only covered ${Math.round(coveragePercent)}% of topics — work through more areas.`);
143
+ }
144
+
145
+ if (trend === 'improving') {
146
+ parts.push('Your performance is improving — keep up the momentum!');
147
+ } else if (trend === 'declining') {
148
+ parts.push('Your recent scores are declining. Consider reviewing earlier material.');
149
+ }
150
+
151
+ if (daysRemaining > 14) {
152
+ parts.push('You have time to make significant improvements with consistent daily practice.');
153
+ } else if (daysRemaining > 7) {
154
+ parts.push('Focus on weak areas and complete at least one full mock exam.');
155
+ } else {
156
+ parts.push('Final stretch: review key concepts, rest well, and trust your preparation.');
157
+ }
158
+
159
+ return parts.join(' ');
160
+ }
161
+
162
+ export function calculateReadiness(
163
+ profileId: string,
164
+ examType: string,
165
+ examDate: string
166
+ ): ReadinessReport {
167
+ const allTopicStats = getTopicStats(profileId);
168
+ const examTopics = EXAM_TOPICS[examType] || [];
169
+
170
+ // Get exam weight data from questions
171
+ const topicWeights: Record<string, number> = {};
172
+ for (const q of QUESTION_BANK) {
173
+ if (q.subject === examType) {
174
+ if (!topicWeights[q.topic] || q.examWeight > topicWeights[q.topic]) {
175
+ topicWeights[q.topic] = q.examWeight;
176
+ }
177
+ }
178
+ }
179
+
180
+ // Calculate per-topic accuracy
181
+ const topicScores: TopicScore[] = [];
182
+ let weightedScore = 0;
183
+ let totalWeight = 0;
184
+
185
+ for (const stats of allTopicStats) {
186
+ if (stats.total_attempts === 0) continue;
187
+ const accuracy = (stats.correct_attempts / stats.total_attempts) * 100;
188
+ const weight = topicWeights[stats.topic] || 0.5;
189
+
190
+ topicScores.push({
191
+ topic: stats.topic,
192
+ accuracy,
193
+ attempts: stats.total_attempts
194
+ });
195
+
196
+ weightedScore += accuracy * weight;
197
+ totalWeight += weight;
198
+ }
199
+
200
+ const baseScore = totalWeight > 0 ? weightedScore / totalWeight : 0;
201
+
202
+ // Coverage bonus
203
+ const practicedTopics = new Set(allTopicStats.filter(s => s.total_attempts > 0).map(s => s.topic));
204
+ const examSpecificTopics = examTopics.length > 0 ? examTopics : [...practicedTopics];
205
+ const coveragePercent = examSpecificTopics.length > 0
206
+ ? (practicedTopics.size / examSpecificTopics.length) * 100
207
+ : 0;
208
+
209
+ let coverageBonus = 0;
210
+ if (coveragePercent >= 80) coverageBonus = 20;
211
+ else if (coveragePercent >= 60) coverageBonus = 10;
212
+
213
+ // Trend calculation from recent sessions
214
+ const recentSessions = getRecentSessions(profileId, 20);
215
+ let trend: 'improving' | 'declining' | 'stable' = 'stable';
216
+ let trendModifier = 0;
217
+
218
+ if (recentSessions.length >= 6) {
219
+ const recent5 = recentSessions.slice(0, 5);
220
+ const prev5 = recentSessions.slice(5, 10);
221
+
222
+ const recentAccuracy = recent5.reduce((sum, s) =>
223
+ sum + (s.questions_answered > 0 ? s.correct / s.questions_answered : 0), 0) / recent5.length;
224
+ const prevAccuracy = prev5.reduce((sum, s) =>
225
+ sum + (s.questions_answered > 0 ? s.correct / s.questions_answered : 0), 0) / prev5.length;
226
+
227
+ const diff = recentAccuracy - prevAccuracy;
228
+ if (diff > 0.05) {
229
+ trend = 'improving';
230
+ trendModifier = 5;
231
+ } else if (diff < -0.05) {
232
+ trend = 'declining';
233
+ trendModifier = -5;
234
+ }
235
+ }
236
+
237
+ // Time modifier
238
+ const now = new Date();
239
+ const examDateObj = new Date(examDate);
240
+ const daysRemaining = Math.max(0, Math.ceil((examDateObj.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)));
241
+ const timeModifier = daysRemaining > 30 ? 3 : daysRemaining > 14 ? 1 : 0;
242
+
243
+ // Final score (clamped 0-100)
244
+ const rawScore = baseScore + (coverageBonus * 0.3) + trendModifier + timeModifier;
245
+ const score = Math.max(0, Math.min(100, rawScore));
246
+
247
+ // Identify strong and weak topics
248
+ const strongTopics = topicScores
249
+ .filter(t => t.accuracy >= 80)
250
+ .sort((a, b) => b.accuracy - a.accuracy);
251
+
252
+ const weakTopics = topicScores
253
+ .filter(t => t.accuracy < 60)
254
+ .sort((a, b) => a.accuracy - b.accuracy);
255
+
256
+ const recommendation = generateRecommendation(score, weakTopics, coveragePercent, trend, daysRemaining);
257
+ const estimatedGrade = getGradeEstimate(examType, score);
258
+
259
+ return {
260
+ score: Math.round(score),
261
+ strongTopics,
262
+ weakTopics,
263
+ coveragePercent: Math.round(coveragePercent),
264
+ trend,
265
+ recommendation,
266
+ estimatedGrade
267
+ };
268
+ }
@@ -0,0 +1,364 @@
1
+ export interface StudyPlan {
2
+ examType: string;
3
+ examDate: string;
4
+ daysRemaining: number;
5
+ dailySchedule: DayPlan[];
6
+ topicsIdentified: string[];
7
+ weeklyGoals: string[];
8
+ }
9
+
10
+ export interface DayPlan {
11
+ date: string;
12
+ dayNumber: number;
13
+ activities: Activity[];
14
+ estimatedMinutes: number;
15
+ isMockExam: boolean;
16
+ }
17
+
18
+ export interface Activity {
19
+ topic: string;
20
+ type: 'study' | 'practice' | 'review' | 'mock_exam';
21
+ durationMinutes: number;
22
+ description: string;
23
+ }
24
+
25
+ // Topic lists per exam type
26
+ export const EXAM_TOPICS: Record<string, string[]> = {
27
+ AP_Biology: [
28
+ 'Cell Structure',
29
+ 'DNA Replication',
30
+ 'Transcription and Translation',
31
+ 'Cell Division',
32
+ 'Mendelian Genetics',
33
+ 'Evolution and Natural Selection',
34
+ 'Ecology',
35
+ 'Photosynthesis',
36
+ 'Cellular Respiration',
37
+ 'Plant Systems',
38
+ 'Animal Systems',
39
+ 'Genetics',
40
+ ],
41
+ AP_Chemistry: [
42
+ 'Atomic Structure',
43
+ 'Chemical Bonding',
44
+ 'Stoichiometry',
45
+ 'Gas Laws',
46
+ 'Thermodynamics',
47
+ 'Chemical Equilibrium',
48
+ 'Acids and Bases',
49
+ 'Electrochemistry',
50
+ 'Kinetics',
51
+ 'Nuclear Chemistry',
52
+ ],
53
+ AP_US_History: [
54
+ 'Colonial Period',
55
+ 'American Revolution',
56
+ 'Constitutional Convention',
57
+ 'Early Republic',
58
+ 'Antebellum and Civil War',
59
+ 'Reconstruction Era',
60
+ 'Gilded Age',
61
+ 'Progressive Era',
62
+ 'World War I',
63
+ 'World War II',
64
+ 'Cold War',
65
+ 'Civil Rights Movement',
66
+ 'Modern America',
67
+ ],
68
+ AP_World_History: [
69
+ 'Ancient Civilizations',
70
+ 'Classical Period',
71
+ 'Post-Classical Period',
72
+ 'Renaissance and Reformation',
73
+ 'Early Modern Period',
74
+ 'Industrialization',
75
+ 'Imperialism',
76
+ 'World War I',
77
+ 'World War II',
78
+ 'Cold War',
79
+ 'Globalization',
80
+ ],
81
+ AP_Physics: [
82
+ 'Kinematics',
83
+ 'Newton\'s Laws',
84
+ 'Energy and Work',
85
+ 'Momentum',
86
+ 'Circular Motion',
87
+ 'Gravity',
88
+ 'Electricity',
89
+ 'Magnetism',
90
+ 'Waves and Optics',
91
+ 'Thermodynamics',
92
+ ],
93
+ AP_Calculus: [
94
+ 'Limits and Continuity',
95
+ 'Derivatives',
96
+ 'Applications of Derivatives',
97
+ 'Integration',
98
+ 'Applications of Integration',
99
+ 'Differential Equations',
100
+ 'Sequences and Series',
101
+ ],
102
+ AP_Statistics: [
103
+ 'Exploratory Data Analysis',
104
+ 'Sampling and Experiments',
105
+ 'Probability',
106
+ 'Probability Distributions',
107
+ 'Sampling Distributions',
108
+ 'Confidence Intervals',
109
+ 'Hypothesis Testing',
110
+ 'Regression',
111
+ ],
112
+ AP_English: [
113
+ 'Rhetorical Analysis',
114
+ 'Argument and Evidence',
115
+ 'Synthesis Essays',
116
+ 'Literary Analysis',
117
+ 'Close Reading',
118
+ 'Grammar and Style',
119
+ ],
120
+ AP_Economics: [
121
+ 'Supply and Demand',
122
+ 'Elasticity',
123
+ 'Consumer Theory',
124
+ 'Production and Costs',
125
+ 'Market Structures',
126
+ 'GDP and Macroeconomics',
127
+ 'Money and Banking',
128
+ 'Monetary Policy',
129
+ 'Fiscal Policy',
130
+ 'International Trade',
131
+ ],
132
+ SAT_Math: [
133
+ 'Linear Equations',
134
+ 'Linear Inequalities',
135
+ 'Systems of Equations',
136
+ 'Quadratic Equations',
137
+ 'Percentages and Proportions',
138
+ 'Statistics and Probability',
139
+ 'Geometry',
140
+ 'Word Problems',
141
+ 'Functions',
142
+ 'Data Analysis',
143
+ ],
144
+ SAT_Reading: [
145
+ 'Main Idea and Purpose',
146
+ 'Evidence and Support',
147
+ 'Vocabulary in Context',
148
+ 'Inference Questions',
149
+ 'Passage Structure',
150
+ 'Comparing Passages',
151
+ 'Data Interpretation',
152
+ ],
153
+ SAT_Writing: [
154
+ 'Grammar Rules',
155
+ 'Sentence Structure',
156
+ 'Punctuation',
157
+ 'Word Choice',
158
+ 'Organization and Transitions',
159
+ 'Evidence and Clarity',
160
+ ],
161
+ MCAT: [
162
+ 'Biology and Biochemistry',
163
+ 'Chemistry and Physics',
164
+ 'Psychology and Sociology',
165
+ 'Critical Analysis',
166
+ 'Research Methods',
167
+ 'Statistics',
168
+ ],
169
+ LSAT: [
170
+ 'Logical Reasoning',
171
+ 'Analytical Reasoning (Logic Games)',
172
+ 'Reading Comprehension',
173
+ 'Argument Structure',
174
+ 'Formal Logic',
175
+ ],
176
+ GRE: [
177
+ 'Verbal Reasoning',
178
+ 'Quantitative Reasoning',
179
+ 'Analytical Writing',
180
+ 'Algebra',
181
+ 'Geometry',
182
+ 'Data Analysis',
183
+ 'Reading Comprehension',
184
+ ],
185
+ };
186
+
187
+ // Exam time limits in minutes
188
+ export const EXAM_TIME_LIMITS: Record<string, number> = {
189
+ AP_Biology: 170,
190
+ AP_Chemistry: 190,
191
+ AP_US_History: 205,
192
+ AP_World_History: 205,
193
+ AP_Physics: 180,
194
+ AP_Calculus: 195,
195
+ AP_Statistics: 180,
196
+ AP_English: 215,
197
+ AP_Economics: 130,
198
+ SAT_Math: 70,
199
+ SAT_Reading: 65,
200
+ SAT_Writing: 35,
201
+ MCAT: 375,
202
+ LSAT: 175,
203
+ GRE: 225,
204
+ };
205
+
206
+ export const MOCK_EXAM_QUESTIONS: Record<string, number> = {
207
+ AP_Biology: 60,
208
+ AP_Chemistry: 60,
209
+ AP_US_History: 55,
210
+ AP_World_History: 55,
211
+ AP_Physics: 50,
212
+ AP_Calculus: 45,
213
+ AP_Statistics: 40,
214
+ AP_English: 45,
215
+ AP_Economics: 60,
216
+ SAT_Math: 44,
217
+ SAT_Reading: 52,
218
+ SAT_Writing: 44,
219
+ MCAT: 230,
220
+ LSAT: 100,
221
+ GRE: 80,
222
+ };
223
+
224
+ export function generateStudyPlan(
225
+ examType: string,
226
+ examDate: string,
227
+ currentLevel: 'beginner' | 'intermediate' | 'advanced',
228
+ weakTopics?: string[]
229
+ ): StudyPlan {
230
+ const now = new Date();
231
+ const examDateObj = new Date(examDate);
232
+ const msPerDay = 1000 * 60 * 60 * 24;
233
+ const daysRemaining = Math.max(1, Math.ceil((examDateObj.getTime() - now.getTime()) / msPerDay));
234
+
235
+ const allTopics = EXAM_TOPICS[examType] || ['General Study'];
236
+ const topicsIdentified = [...allTopics];
237
+
238
+ // Level-based settings
239
+ const minutesPerTopic = currentLevel === 'beginner' ? 60 : currentLevel === 'intermediate' ? 45 : 30;
240
+ const topicsPerDay = currentLevel === 'beginner' ? 1 : currentLevel === 'intermediate' ? 2 : 3;
241
+
242
+ // Build topic schedule with weak areas weighted 2x
243
+ const weightedTopics: string[] = [];
244
+ for (const topic of allTopics) {
245
+ const isWeak = weakTopics && weakTopics.includes(topic);
246
+ weightedTopics.push(topic);
247
+ if (isWeak) {
248
+ weightedTopics.push(topic); // Add again for 2x weight
249
+ }
250
+ }
251
+
252
+ // Build daily schedule
253
+ const dailySchedule: DayPlan[] = [];
254
+ let topicIndex = 0;
255
+ const weeklyGoals: string[] = [];
256
+
257
+ for (let day = 0; day < Math.min(daysRemaining, 90); day++) {
258
+ const date = new Date(now);
259
+ date.setDate(date.getDate() + day);
260
+ const dateStr = date.toISOString().split('T')[0];
261
+ const dayNumber = day + 1;
262
+
263
+ // Mock exams at 7 and 3 days before exam
264
+ const daysBeforeExam = daysRemaining - day;
265
+ const isMockExam = daysBeforeExam === 7 || daysBeforeExam === 3;
266
+
267
+ const activities: Activity[] = [];
268
+ let estimatedMinutes = 0;
269
+
270
+ if (isMockExam) {
271
+ const timeLimitMins = EXAM_TIME_LIMITS[examType] || 120;
272
+ activities.push({
273
+ topic: 'Full Mock Exam',
274
+ type: 'mock_exam',
275
+ durationMinutes: timeLimitMins + 30,
276
+ description: `Full-length ${examType} practice exam simulating real conditions. Review all answers afterward.`
277
+ });
278
+ estimatedMinutes = timeLimitMins + 30;
279
+ } else if (daysBeforeExam === 1) {
280
+ // Day before: light review
281
+ activities.push({
282
+ topic: 'Final Review',
283
+ type: 'review',
284
+ durationMinutes: 30,
285
+ description: 'Light review of key formulas and concepts. Rest and prepare for exam day.'
286
+ });
287
+ estimatedMinutes = 30;
288
+ } else {
289
+ // Regular study day
290
+ const dayTopics = weightedTopics.slice(
291
+ topicIndex % weightedTopics.length,
292
+ (topicIndex % weightedTopics.length) + topicsPerDay
293
+ );
294
+ topicIndex += topicsPerDay;
295
+
296
+ // Deduplicate topics for this day
297
+ const uniqueDayTopics = [...new Set(dayTopics)];
298
+
299
+ for (const topic of uniqueDayTopics) {
300
+ const isWeak = weakTopics && weakTopics.includes(topic);
301
+
302
+ activities.push({
303
+ topic,
304
+ type: 'study',
305
+ durationMinutes: minutesPerTopic,
306
+ description: `Study ${topic} — ${isWeak ? 'focus area (weak topic)' : 'core concepts and examples'}`
307
+ });
308
+ estimatedMinutes += minutesPerTopic;
309
+
310
+ // Add practice component
311
+ activities.push({
312
+ topic,
313
+ type: 'practice',
314
+ durationMinutes: 15,
315
+ description: `Practice questions for ${topic}`
316
+ });
317
+ estimatedMinutes += 15;
318
+ }
319
+
320
+ // Weekly review on Sundays
321
+ if (date.getDay() === 0) {
322
+ activities.push({
323
+ topic: 'Weekly Review',
324
+ type: 'review',
325
+ durationMinutes: 30,
326
+ description: 'Review all topics covered this week. Identify areas needing more attention.'
327
+ });
328
+ estimatedMinutes += 30;
329
+ }
330
+ }
331
+
332
+ dailySchedule.push({
333
+ date: dateStr,
334
+ dayNumber,
335
+ activities,
336
+ estimatedMinutes,
337
+ isMockExam
338
+ });
339
+ }
340
+
341
+ // Generate weekly goals
342
+ const weeksRemaining = Math.ceil(daysRemaining / 7);
343
+ const topicsPerWeek = Math.ceil(allTopics.length / Math.max(weeksRemaining, 1));
344
+
345
+ for (let week = 0; week < Math.min(weeksRemaining, 6); week++) {
346
+ const weekTopics = allTopics.slice(week * topicsPerWeek, (week + 1) * topicsPerWeek);
347
+ if (weekTopics.length > 0) {
348
+ weeklyGoals.push(`Week ${week + 1}: Master ${weekTopics.join(', ')}`);
349
+ }
350
+ }
351
+
352
+ if (weeksRemaining > 0) {
353
+ weeklyGoals.push(`Final week: Full mock exams and comprehensive review`);
354
+ }
355
+
356
+ return {
357
+ examType,
358
+ examDate,
359
+ daysRemaining,
360
+ dailySchedule,
361
+ topicsIdentified,
362
+ weeklyGoals
363
+ };
364
+ }