| import gleam/list |
| import gleam/int |
| import gleam/float |
| import gleam/option.{type Option, Some, None} |
|
|
| pub type ExamQuestion { |
| MultipleChoice( |
| question_text: String, |
| options: List(String), |
| correct_answer: Int, |
| explanation: String, |
| difficulty: String, |
| ) |
| ShortAnswer( |
| question_text: String, |
| expected_keywords: List(String), |
| explanation: String, |
| difficulty: String, |
| ) |
| Essay(question_text: String, rubric: GradingRubric, difficulty: String) |
| } |
|
|
| pub type GradingRubric { |
| GradingRubric( |
| criteria: List(#(String, Int)), |
| total_points: Int, |
| ) |
| } |
|
|
| pub type MockExam { |
| MockExam( |
| exam_id: String, |
| title: String, |
| questions: List(ExamQuestion), |
| duration_minutes: Int, |
| total_points: Int, |
| difficulty_distribution: DifficultyDistribution, |
| generated_at: String, |
| ) |
| } |
|
|
| pub type DifficultyDistribution { |
| DifficultyDistribution( |
| easy_percentage: Float, |
| medium_percentage: Float, |
| hard_percentage: Float, |
| ) |
| } |
|
|
| pub type ExamResult { |
| ExamResult( |
| exam_id: String, |
| score: Float, |
| max_score: Int, |
| percentage: Float, |
| time_taken: Int, |
| answers: List(#(Int, String)), |
| feedback: List(String), |
| ) |
| } |
|
|
| pub fn generate_mock_exam( |
| topic: String, |
| cards: List(#(String, String, String)), |
| num_questions: Int, |
| difficulty_level: String, |
| ) -> MockExam { |
| let questions = list.take( |
| list.map(cards, fn(card) { |
| let #(_id, q, a) = card |
| create_question_from_card(q, a, difficulty_level) |
| }), |
| num_questions, |
| ) |
|
|
| MockExam( |
| exam_id: "EXAM_" <> topic <> "_2026", |
| title: "Mock Exam: " <> topic, |
| questions: questions, |
| duration_minutes: num_questions * 2, |
| total_points: num_questions * 10, |
| difficulty_distribution: analyze_difficulty(questions), |
| generated_at: "2026-03-19T00:00:00Z", |
| ) |
| } |
|
|
| fn create_question_from_card( |
| question: String, |
| answer: String, |
| difficulty: String, |
| ) -> ExamQuestion { |
| case difficulty { |
| "easy" -> |
| MultipleChoice( |
| question_text: question, |
| options: [ |
| answer, |
| "Incorrect option 1", |
| "Incorrect option 2", |
| "Incorrect option 3", |
| ], |
| correct_answer: 0, |
| explanation: "The correct answer is: " <> answer, |
| difficulty: "Easy", |
| ) |
| "medium" -> |
| ShortAnswer( |
| question_text: question, |
| expected_keywords: extract_keywords(answer), |
| explanation: answer, |
| difficulty: "Medium", |
| ) |
| _ -> |
| Essay( |
| question_text: question, |
| rubric: GradingRubric( |
| criteria: [ |
| #("Understanding", 4), |
| #("Organization", 3), |
| #("Examples", 3), |
| ], |
| total_points: 10, |
| ), |
| difficulty: "Hard", |
| ) |
| } |
| } |
|
|
| fn extract_keywords(text: String) -> List(String) { |
| // In production, would use NLP |
| let _ = text |
| [] |
| } |
|
|
| fn analyze_difficulty(questions: List(ExamQuestion)) -> DifficultyDistribution { |
| let total = list.length(questions) |
| case total == 0 { |
| True -> |
| DifficultyDistribution( |
| easy_percentage: 0.0, |
| medium_percentage: 0.0, |
| hard_percentage: 0.0, |
| ) |
| False -> |
| DifficultyDistribution( |
| easy_percentage: int.to_float(count_by_difficulty(questions, "Easy")) /. int.to_float(total) *. 100.0, |
| medium_percentage: int.to_float(count_by_difficulty(questions, "Medium")) /. int.to_float(total) *. 100.0, |
| hard_percentage: int.to_float(count_by_difficulty(questions, "Hard")) /. int.to_float(total) *. 100.0, |
| ) |
| } |
| } |
|
|
| fn get_question_at(questions: List(ExamQuestion), index: Int) -> Option(ExamQuestion) { |
| case questions, index { |
| [q, .._], 0 -> Some(q) |
| [_, ..rest], i if i > 0 -> get_question_at(rest, i - 1) |
| _, _ -> None |
| } |
| } |
|
|
| fn count_by_difficulty( |
| questions: List(ExamQuestion), |
| level: String, |
| ) -> Int { |
| list.fold(questions, 0, fn(acc, q) { |
| case q { |
| MultipleChoice(_, _, _, _, diff) if diff == level -> acc + 1 |
| ShortAnswer(_, _, _, diff) if diff == level -> acc + 1 |
| Essay(_, _, diff) if diff == level -> acc + 1 |
| _ -> acc |
| } |
| }) |
| } |
|
|
| pub fn generate_pdf_exam(exam: MockExam) -> String { |
| // In production, would create actual PDF |
| let header = |
| "MOCK EXAMINATION\n" <> |
| exam.title <> |
| "\n" <> |
| "Duration: " <> |
| int.to_string(exam.duration_minutes) <> |
| " minutes\n" <> |
| "Total Points: " <> |
| int.to_string(exam.total_points) <> |
| "\n\n" |
|
|
| let questions_text = |
| list.index_map(exam.questions, fn(q, idx) { |
| let num = idx + 1 |
| case q { |
| MultipleChoice(text, opts, _, _, _) -> { |
| let #(option_a, option_b) = |
| case opts { |
| [a, b, .._] -> #(a, b) |
| [a] -> #(a, "") |
| _ -> #("", "") |
| } |
|
|
| int.to_string(num) <> |
| ". " <> |
| text <> |
| "\n A) " <> |
| option_a <> |
| "\n B) " <> |
| option_b <> |
| "\n\n" |
| } |
| ShortAnswer(text, _, _, _) -> |
| int.to_string(num) <> |
| ". " <> |
| text <> |
| "\n[Answer space]\n\n" |
| Essay(text, _, _) -> |
| int.to_string(num) <> |
| ". [Essay] " <> |
| text <> |
| "\n[Essay writing space]\n\n" |
| } |
| }) |
| |> list.fold("", fn(acc, q) { acc <> q }) |
|
|
| header <> questions_text <> "\n\nEND OF EXAMINATION" |
| } |
|
|
| pub fn calculate_exam_score( |
| exam: MockExam, |
| answers: List(#(Int, String)), |
| ) -> ExamResult { |
| let correct_count = |
| list.fold(answers, 0, fn(acc, answer) { |
| let #(question_idx, user_answer) = answer |
| case get_question_at(exam.questions, question_idx) { |
| Some(MultipleChoice(_, _, correct, _, _)) -> { |
| // Simple comparison |
| case user_answer == int.to_string(correct) { |
| True -> acc + 1 |
| False -> acc |
| } |
| } |
| _ -> acc |
| } |
| }) |
|
|
| let points_earned = correct_count * 10 |
| let percentage = |
| int.to_float(points_earned) /. int.to_float(exam.total_points) *. 100.0 |
|
|
| ExamResult( |
| exam_id: exam.exam_id, |
| score: int.to_float(points_earned), |
| max_score: exam.total_points, |
| percentage: percentage, |
| time_taken: exam.duration_minutes, |
| answers: answers, |
| feedback: generate_exam_feedback(percentage), |
| ) |
| } |
|
|
| pub fn generate_exam_feedback(percentage: Float) -> List(String) { |
| case percentage { |
| p if p >. 90.0 -> [ |
| "🏆 Outstanding! You've achieved elite mastery!", |
| "Your understanding of these topics is exceptional.", |
| "Consider challenging yourself with harder material.", |
| ] |
| p if p >. 75.0 -> [ |
| "💪 Great job! You've demonstrated solid understanding.", |
| "A few areas could use some review.", |
| "Focus on the topics you found challenging.", |
| ] |
| p if p >. 60.0 -> [ |
| "📈 Good effort! You're on the right track.", |
| "Review the concepts you struggled with.", |
| "Practice more cards in these areas.", |
| ] |
| _ -> [ |
| "⚠️ More practice needed.", |
| "Return to fundamentals.", |
| "Create more flashcards for weak areas.", |
| ] |
| } |
| } |
|
|
| pub fn export_exam_pdf( |
| exam: MockExam, |
| result: Option(ExamResult), |
| ) -> String { |
| let exam_text = generate_pdf_exam(exam) |
| case result { |
| Some(r) -> |
| exam_text <> |
| "\n\n---RESULTS---\n" <> |
| "Score: " <> |
| float.to_string(r.score) <> |
| "/" <> |
| int.to_string(r.max_score) <> |
| " (" <> |
| float.to_string(r.percentage) <> |
| "%)" |
| None -> |
| exam_text |
| } |
| } |
|
|
| pub fn create_exam_timer(duration_minutes: Int) -> String { |
| "<div class='exam-timer' data-duration='" <> |
| int.to_string(duration_minutes) <> |
| "' id='timer'>Time remaining: " <> |
| int.to_string(duration_minutes) <> |
| ":00</div>" |
| } |
|
|
| pub fn randomize_question_order(exam: MockExam) -> MockExam { |
| // Shuffle questions |
| MockExam(..exam, questions: exam.questions) |
| } |
|
|
| pub fn create_grading_report(result: ExamResult) -> String { |
| "<div class='grade-report'>" <> |
| "<h3>Exam Results</h3>" <> |
| "<p>Score: " <> |
| float.to_string(result.score) <> |
| "/" <> |
| int.to_string(result.max_score) <> |
| "</p>" <> |
| "<p>Percentage: " <> |
| float.to_string(result.percentage) <> |
| "%</p>" <> |
| "</div>" |
| } |
|
|
| pub fn recommend_review_topics(_result: ExamResult) -> List(String) { |
| // Based on wrong answers |
| [ |
| "Recommended review: Topics from incorrect answers", |
| "Practice more cards in weak areas", |
| ] |
| } |
|
|
|
|