library(shiny)
library(tm)
## Loading required package: NLP
library(stringr)
This Shiny document implements QuizNuggets, a simple app that: 1. Uploads a .txt or .pdf of class notes 2. Extracts top keywords 3. Generates multiple‐choice questions (MCQs) and flashcards 4. Allows downloading MCQs and flashcards as CSV files
new_QuizNuggetsA1 <- function(file_path) {
# trying to make sure the file path is valid
if (missing(file_path) || !file.exists(file_path)) {
stop("Provide a valid file path.")
}
ext <- tools::file_ext(file_path)
raw_txt <- NULL
if (tolower(ext) == "txt") {
# trying to read a TXT file
raw_txt <- tryCatch({
paste(readLines(file_path, warn = FALSE), collapse = " ")
}, error = function(e) {
stop("Error reading TXT: ", e$message)
})
} else if (tolower(ext) == "pdf") {
# trying to read a PDF file by picking up text from the PDF corpus
raw_txt <- tryCatch({
cr <- Corpus(URISource(file_path), readerControl = list(reader = readPDF))
paste(unlist(sapply(cr, function(x) x$content)), collapse = " ")
}, error = function(e) {
stop("Error reading PDF: ", e$message)
})
} else {
stop("Use .txt or .pdf.")
}
obj <- list(
raw_text = raw_txt,
keywords = NULL,
questions = NULL,
flashcards = NULL
)
class(obj) <- "QuizNuggetsA1"
obj
}
# Print method for QuizNuggetsA1
print.QuizNuggetsA1 <- function(x, ...) {
cat("QuizNuggetsA1 object\n")
cat(" Text length:", nchar(x$raw_text), "\n")
if (!is.null(x$keywords)) cat(" Keywords:", nrow(x$keywords), "\n")
if (!is.null(x$questions)) cat(" Questions:", nrow(x$questions), "\n")
if (!is.null(x$flashcards)) cat(" Flashcards:", nrow(x$flashcards), "\n")
}
# text cleaning: lowercase, remove punctuation, numbers, extra whitespace
clean_textA1 <- function(text_input) {
# trying to ensure input is text
if (!is.character(text_input)) stop("Need character string.")
# picking up raw text and cleaning it
t1 <- tolower(text_input)
t1 <- removePunctuation(t1)
t1 <- removeNumbers(t1)
stripWhitespace(t1)
}
# compute word frequencies (excluding English stopwords)
word_freqA1 <- function(clean_text) {
# trying to get word frequencies without stopwords
if (!is.character(clean_text)) stop("Need character string.")
wv <- unlist(str_split(clean_text, "\\s+"))
sw <- stopwords("en")
wv <- wv[!(wv %in% sw)]
df <- as.data.frame(table(wv), stringsAsFactors = FALSE)
colnames(df) <- c("term", "freq")
df[order(-df$freq), , drop = FALSE]
}
# select top N keywords from frequency data.frame
top_keywordsA1 <- function(freq_df, top_n = 10) {
# trying to pick up the top N keywords
if (!is.data.frame(freq_df)) stop("Need data frame.")
top_n <- min(top_n, nrow(freq_df))
freq_df[1:top_n, , drop = FALSE]
}
# generate MCQs by sampling random sentences containing keywords
gen_mcqA1 <- function(raw_text, keywords_df, num_choices = 4) {
# trying to skim through unique sentences and generate MCQs
if (!is.character(raw_text)) stop("Need raw text.")
if (!is.data.frame(keywords_df)) stop("Need keywords data frame.")
# splitting text into sentences and picking up unique ones
all_sents <- unlist(str_split(raw_text, "(?<=[\\.\\?!])\\s+"))
uniq_sents <- unique(all_sents)
# deciding how many questions to make
n_questions <- min(nrow(keywords_df), length(uniq_sents))
set.seed(123)
sampled_sents <- sample(uniq_sents, n_questions)
Q_df <- data.frame(
question = character(),
correct_answer = character(),
opts = character(),
stringsAsFactors = FALSE
)
for (i in seq_len(n_questions)) {
sent_i <- sampled_sents[i]
# picking up which keywords appear in this sentence
found_terms <- keywords_df$term[
str_detect(sent_i, regex(paste0("\\b(", paste(keywords_df$term, collapse = "|"), ")\\b"), ignore_case = TRUE))
]
if (length(found_terms) == 0) next
# trying to pick one keyword at random from those found
term_i <- sample(found_terms, 1)
# replacing that keyword with a blank
pat <- paste0("\\b", str_replace_all(term_i, "([\\.\\+\\*\\?\\^\\$\\(\\)\\[\\]\\{\\}\\|\\\\])", "\\\\\\1"), "\\b")
blank_sent <- str_replace_all(sent_i, regex(pat, ignore_case = TRUE), "____")
# picking up wrong options from other keywords
others <- setdiff(keywords_df$term, term_i)
if (length(others) < (num_choices - 1)) next
wrong <- sample(others, num_choices - 1)
all_opts <- sample(c(term_i, wrong))
opt_str <- paste(all_opts, collapse = " | ")
Q_df <- rbind(
Q_df,
data.frame(
question = blank_sent,
correct_answer = term_i,
opts = opt_str,
stringsAsFactors = FALSE
)
)
}
# numbering each question
if (nrow(Q_df) > 0) {
Q_df$question <- paste0(seq_len(nrow(Q_df)), ". ", Q_df$question)
}
rownames(Q_df) <- NULL
Q_df
}
# generate flashcards by sampling a random sentence containing each keyword
gen_flashcardsA1 <- function(raw_text, keywords_df) {
# trying to skim through unique sentences and generate flashcards
if (!is.character(raw_text)) stop("Need raw text.")
if (!is.data.frame(keywords_df)) stop("Need keywords data frame.")
# splitting and picking up unique sentences
all_sents <- unlist(str_split(raw_text, "(?<=[\\.\\?!])\\s+"))
uniq_sents <- unique(all_sents)
FC_df <- data.frame(
term = character(),
context = character(),
stringsAsFactors = FALSE
)
for (i in seq_len(nrow(keywords_df))) {
term_i <- keywords_df$term[i]
pat <- paste0("\\b", str_replace_all(term_i, "([\\.\\+\\*\\?\\^\\$\\(\\)\\[\\]\\{\\}\\|\\\\])", "\\\\\\1"), "\\b")
matches <- uniq_sents[str_detect(uniq_sents, regex(pat, ignore_case = TRUE))]
if (length(matches) == 0) next
# picking up a random sentence as context
chosen <- sample(matches, 1)
FC_df <- rbind(
FC_df,
data.frame(
term = paste0(i, ". ", term_i),
context = chosen,
stringsAsFactors = FALSE
)
)
}
rownames(FC_df) <- NULL
FC_df
}
# S3 method to extract keywords
extract_keywords.QuizNuggetsA1 <- function(obj, top_n = 10) {
# trying to get top keywords if not already there
if (!inherits(obj, "QuizNuggetsA1")) stop("Wrong class.")
ct <- clean_textA1(obj$raw_text)
wf <- word_freqA1(ct)
obj$keywords <- top_keywordsA1(wf, top_n)
obj
}
# S3 method to generate MCQs
generate_mcq.QuizNuggetsA1 <- function(obj, top_n = 10, num_choices = 4) {
# trying to generate MCQs for the object
if (!inherits(obj, "QuizNuggetsA1")) stop("Wrong class.")
if (is.null(obj$keywords)) obj <- extract_keywords.QuizNuggetsA1(obj, top_n)
obj$questions <- gen_mcqA1(obj$raw_text, obj$keywords, num_choices)
obj
}
# S3 method to generate flashcards
generate_flashcards.QuizNuggetsA1 <- function(obj, top_n = 10) {
# trying to generate flashcards for the object
if (!inherits(obj, "QuizNuggetsA1")) stop("Wrong class.")
if (is.null(obj$keywords)) obj <- extract_keywords.QuizNuggetsA1(obj, top_n)
obj$flashcards <- gen_flashcardsA1(obj$raw_text, obj$keywords)
obj
}
ui <- fluidPage(
titlePanel("QuizNuggets"),
sidebarLayout(
sidebarPanel(
fileInput("file_upload", "Upload notes (TXT or PDF)", accept = c(".txt", ".pdf")),
numericInput("num_keywords", "Top keywords:", value = 10, min = 5, max = 50, step = 5),
numericInput("num_choices", "Choices per Q:", value = 4, min = 2, max = 5, step = 1),
actionButton("go_button", "Generate Quiz & Flashcards"),
hr(),
downloadButton("download_quiz_csv", "Download Quiz CSV"),
br(), br(),
downloadButton("download_flashcards_csv", "Download Flashcards CSV")
),
mainPanel(
h4("MCQs:"),
tableOutput("quiz_table"),
hr(),
h4("Flashcards:"),
tableOutput("flashcard_table")
)
)
)
server <- function(input, output, session) {
rv <- reactiveValues(quiz_obj = NULL)
observeEvent(input$go_button, {
req(input$file_upload)
tmp <- input$file_upload$datapath
obj <- tryCatch(new_QuizNuggetsA1(tmp), error = function(e) {
showNotification(paste("Read error:", e$message), type = "error")
NULL
})
if (is.null(obj)) return()
obj <- tryCatch(generate_mcq.QuizNuggetsA1(obj, top_n = input$num_keywords, num_choices = input$num_choices),
error = function(e) {
showNotification(paste("MCQ error:", e$message), type = "error")
NULL
})
if (is.null(obj)) return()
obj <- tryCatch(generate_flashcards.QuizNuggetsA1(obj, top_n = input$num_keywords),
error = function(e) {
showNotification(paste("Flashcard error:", e$message), type = "error")
NULL
})
if (is.null(obj)) return()
rv$quiz_obj <- obj
})
output$quiz_table <- renderTable({
if (is.null(rv$quiz_obj) || is.null(rv$quiz_obj$questions)) return(NULL)
df <- rv$quiz_obj$questions
opts_split <- str_split(df$opts, " \\| ")
max_opts <- max(sapply(opts_split, length))
opts_mat <- t(sapply(opts_split, function(x) {
c(x, rep("", max_opts - length(x)))
}))
mcq_df <- data.frame(
Question = df$question,
opts_mat,
stringsAsFactors = FALSE
)
colnames(mcq_df) <- c("Question", paste0("Option", seq_len(max_opts)))
mcq_df
}, sanitize.text.function = function(x) x)
output$flashcard_table <- renderTable({
if (is.null(rv$quiz_obj) || is.null(rv$quiz_obj$flashcards)) return(NULL)
fc <- rv$quiz_obj$flashcards
colnames(fc) <- c("Term", "Context sentence")
fc
}, sanitize.text.function = function(x) x)
output$download_quiz_csv <- downloadHandler(
filename = function() paste0("quiz_", Sys.Date(), ".csv"),
content = function(file) {
if (is.null(rv$quiz_obj) || is.null(rv$quiz_obj$questions)) {
write.csv(data.frame(), file, row.names = FALSE)
} else {
qs <- rv$quiz_obj$questions
opts_split <- str_split(qs$opts, " \\| ")
max_opts <- max(sapply(opts_split, length))
con <- file(file, open = "w", encoding = "UTF-8")
header <- c("Question", paste0("Option", seq_len(max_opts)))
writeLines(paste(header, collapse = ","), con)
for (i in seq_len(nrow(qs))) {
q_text <- gsub('"', '""', qs$question[i])
opts_vec <- opts_split[[i]]
opts_vec <- c(opts_vec, rep("", max_opts - length(opts_vec)))
opts_vec <- sapply(opts_vec, function(x) gsub('"', '""', x))
line <- paste0('"', q_text, '"', ",", paste0('"', opts_vec, '"', collapse = ","))
writeLines(line, con)
}
# picking up answer key after questions
writeLines("", con)
writeLines("Answer Key", con)
for (i in seq_len(nrow(qs))) {
ans <- gsub('"', '""', qs$correct_answer[i])
writeLines(paste0(i, ',"', ans, '"'), con)
}
close(con)
}
}
)
output$download_flashcards_csv <- downloadHandler(
filename = function() paste0("flashcards_", Sys.Date(), ".csv"),
content = function(file) {
if (is.null(rv$quiz_obj) || is.null(rv$quiz_obj$flashcards)) {
write.csv(data.frame(), file, row.names = FALSE)
} else {
fc <- rv$quiz_obj$flashcards
write.csv(fc, file, row.names = FALSE, quote = TRUE)
}
}
)
}
shinyApp(ui = ui, server = server)