Installing Libraries

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

S3 Class and Helper Functions

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
}

Shiny UI and Server

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)
Shiny applications not supported in static R Markdown documents