flashsync / exam_generator.gleam
Dhurgh's picture
Permanently remove build artifacts from history
d0b8e8f
Raw
History Blame Contribute Delete
8.43 kB
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",
]
}