Final Touches

This commit is contained in:
Landon Taylor 2025-04-18 13:18:48 -06:00
parent 7d98a638b4
commit 3ab839e543
9 changed files with 188 additions and 179 deletions

View File

@ -50,10 +50,9 @@ Contributions are welcome! Please follow the steps at https://gitmoss.fyi/GitMos
## License ## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. This project is licensed under the MIT License. See the [LICENSE](LICENSE.md) file for details.
## Acknowledgments ## Notes
- Inspired by the need for critical thinking in AI education. - Special thanks to Seth Poulsen and his AI Applications in Education course at Utah State University.
- Special thanks to the AI Applications in Education course team at Utah State University. - Portions of this work are produced by generative AI, but all content is manually reviewed and edited.
- Open-source contributors and the developer community.

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{files, learner::profile, printers::{self, clear_console, print_boxed, print_screen}, utilities::questions::{ask_mcq, ask_multi_select, ask_plaintext}, Module}; use crate::{files, learner::profile, printers::{self, clear_console, print_boxed, print_screen}, utilities::{generators::random_affirmation, questions::{ask_mcq, ask_multi_select, ask_plaintext}}, Module};
use super::units; use super::units;
const REQUIRED_FOR_MASTERY: f64 = 0.1; const REQUIRED_FOR_MASTERY: f64 = 0.1;
@ -97,7 +97,7 @@ pub fn inner_loop(unit: Module, learner: &mut profile::Learner) {
print_screen(&screen.text); print_screen(&screen.text);
} }
ScreenType::Mcq => { ScreenType::Mcq => {
let options: Vec<&str> = screen.options.as_ref().unwrap().iter().map(|s| s.as_str()).collect(); // let options: Vec<&str> = screen.options.as_ref().unwrap().iter().map(|s| s.as_str()).collect();
let mut options = screen.options.clone().unwrap(); let mut options = screen.options.clone().unwrap();
options.push("Get hint".to_string()); options.push("Get hint".to_string());
let options: Vec<&str> = options.iter().map(|s| s.as_str()).collect(); let options: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
@ -129,7 +129,7 @@ pub fn inner_loop(unit: Module, learner: &mut profile::Learner) {
} }
} }
ScreenType::Checkboxes => { ScreenType::Checkboxes => {
let options: Vec<&str> = screen.options.as_ref().unwrap().iter().map(|s| s.as_str()).collect(); // let options: Vec<&str> = screen.options.as_ref().unwrap().iter().map(|s| s.as_str()).collect();
let mut options = screen.options.clone().unwrap(); let mut options = screen.options.clone().unwrap();
options.push("Get hint".to_string()); options.push("Get hint".to_string());
let options: Vec<&str> = options.iter().map(|s| s.as_str()).collect(); let options: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
@ -139,7 +139,7 @@ pub fn inner_loop(unit: Module, learner: &mut profile::Learner) {
let selected_indices: Vec<usize> = answers.iter().filter_map(|answer| options.iter().position(|x| *x == *answer)).collect(); let selected_indices: Vec<usize> = answers.iter().filter_map(|answer| options.iter().position(|x| *x == *answer)).collect();
let correct = selected_indices.iter().all(|&index| correct_indices.contains(&index)) && selected_indices.len() == correct_indices.len(); let correct = selected_indices.iter().all(|&index| correct_indices.contains(&index)) && selected_indices.len() == correct_indices.len();
if correct { if correct {
print_boxed("Correct!", printers::StatementType::CorrectFeedback); print_boxed(&format!("Correct! {}", random_affirmation()), printers::StatementType::CorrectFeedback);
learner.update_progress(unit.clone(), true); learner.update_progress(unit.clone(), true);
learner.progress.get_mut(&unit).unwrap().print_progress(); learner.progress.get_mut(&unit).unwrap().print_progress();
break; break;

View File

@ -90,12 +90,12 @@ impl Tracker {
self.milestones_completed as f64 / self.milestones as f64 self.milestones_completed as f64 / self.milestones as f64
} }
pub fn get_milestones(&self) -> usize { // pub fn get_milestones(&self) -> usize {
self.milestones // self.milestones
} // }
pub fn get_milestones_completed(&self) -> usize { // pub fn get_milestones_completed(&self) -> usize {
self.milestones_completed // self.milestones_completed
} // }
pub(crate) fn print_progress(&self) { pub(crate) fn print_progress(&self) {
let (cols, _) = size().unwrap(); let (cols, _) = size().unwrap();

View File

@ -1,8 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use rand::{rng, seq::IndexedRandom}; // use rand::{rng, seq::IndexedRandom};
use std::fs;
use std::path::Path;
// use strum_macros::EnumIter; // use strum_macros::EnumIter;
/// Module defines the structure and data for the course units, including lessons and modules. /// Module defines the structure and data for the course units, including lessons and modules.
@ -40,16 +38,16 @@ impl Module {
Module::Appraisal(appraisal) => format!("data/appraisal/{}.json", appraisal.get_filename()), Module::Appraisal(appraisal) => format!("data/appraisal/{}.json", appraisal.get_filename()),
} }
} }
pub(crate) fn random() -> Self { // pub(crate) fn random() -> Self {
let mut rng = rng(); // let mut rng = rng();
let variants = [ // let variants = [
Module::Logic(Logic::random()), // Module::Logic(Logic::random()),
Module::Fallacy(Fallacy::random()), // Module::Fallacy(Fallacy::random()),
Module::Bias(Bias::random()), // Module::Bias(Bias::random()),
Module::Appraisal(Appraisal::random()), // Module::Appraisal(Appraisal::random()),
]; // ];
variants.choose(&mut rng).unwrap().clone() // variants.choose(&mut rng).unwrap().clone()
} // }
pub fn iter() -> impl Iterator<Item = Self> { pub fn iter() -> impl Iterator<Item = Self> {
vec![ vec![
Module::Introduction, Module::Introduction,
@ -125,11 +123,11 @@ impl Logic {
Logic::BooleanAlgebra => "boolean_algebra".to_string(), Logic::BooleanAlgebra => "boolean_algebra".to_string(),
} }
} }
pub(crate) fn random() -> Self { // pub(crate) fn random() -> Self {
let mut rng = rng(); // let mut rng = rng();
let variants = [Logic::LogicalOperations, Logic::BooleanAlgebra]; // let variants = [Logic::LogicalOperations, Logic::BooleanAlgebra];
variants.choose(&mut rng).unwrap().clone() // variants.choose(&mut rng).unwrap().clone()
} // }
} }
/// Fallacy module, which includes various lessons related to logical fallacies and errors in reasoning. /// Fallacy module, which includes various lessons related to logical fallacies and errors in reasoning.
@ -158,14 +156,14 @@ impl Fallacy {
Fallacy::InformalFallacy(informal) => format!("informal/{}", informal.get_filename()), Fallacy::InformalFallacy(informal) => format!("informal/{}", informal.get_filename()),
} }
} }
pub(crate) fn random() -> Self { // pub(crate) fn random() -> Self {
let mut rng = rng(); // let mut rng = rng();
let variants = [ // let variants = [
Fallacy::FormalFallacy(FormalFallacy::random()), // Fallacy::FormalFallacy(FormalFallacy::random()),
Fallacy::InformalFallacy(InformalFallacy::random()), // Fallacy::InformalFallacy(InformalFallacy::random()),
]; // ];
variants.choose(&mut rng).unwrap().clone() // variants.choose(&mut rng).unwrap().clone()
} // }
} }
/// Formal fallacies, which are errors in the structure of an argument. /// Formal fallacies, which are errors in the structure of an argument.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
@ -199,16 +197,16 @@ impl FormalFallacy {
FormalFallacy::QuantificationalFallacy => "quantificational".to_string(), FormalFallacy::QuantificationalFallacy => "quantificational".to_string(),
} }
} }
pub(crate) fn random() -> Self { // pub(crate) fn random() -> Self {
let mut rng = rng(); // let mut rng = rng();
let variants = [ // let variants = [
FormalFallacy::PropositionalFallacy, // FormalFallacy::PropositionalFallacy,
FormalFallacy::ProbabilisticFallacy, // FormalFallacy::ProbabilisticFallacy,
FormalFallacy::SyllogisticFallacy, // FormalFallacy::SyllogisticFallacy,
FormalFallacy::QuantificationalFallacy, // FormalFallacy::QuantificationalFallacy,
]; // ];
variants.choose(&mut rng).unwrap().clone() // variants.choose(&mut rng).unwrap().clone()
} // }
} }
/// Informal fallacies, which are errors in reasoning that do not involve the structure of the argument. /// Informal fallacies, which are errors in reasoning that do not involve the structure of the argument.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
@ -272,26 +270,26 @@ impl InformalFallacy {
InformalFallacy::BeggingTheQuestion => "begging_the_question".to_string(), InformalFallacy::BeggingTheQuestion => "begging_the_question".to_string(),
} }
} }
pub(crate) fn random() -> Self { // pub(crate) fn random() -> Self {
let mut rng = rng(); // let mut rng = rng();
let variants = [ // let variants = [
InformalFallacy::PostHocErgoPropterHoc, // InformalFallacy::PostHocErgoPropterHoc,
InformalFallacy::SlipperySlope, // InformalFallacy::SlipperySlope,
InformalFallacy::TexasSharpshooter, // InformalFallacy::TexasSharpshooter,
InformalFallacy::HastyGeneralization, // InformalFallacy::HastyGeneralization,
InformalFallacy::OverGeneralization, // InformalFallacy::OverGeneralization,
InformalFallacy::NoTrueScotsman, // InformalFallacy::NoTrueScotsman,
InformalFallacy::QuotingOutOfContext, // InformalFallacy::QuotingOutOfContext,
InformalFallacy::AdHominem, // InformalFallacy::AdHominem,
InformalFallacy::TuQuoque, // InformalFallacy::TuQuoque,
InformalFallacy::Bandwagon, // InformalFallacy::Bandwagon,
InformalFallacy::StrawMan, // InformalFallacy::StrawMan,
InformalFallacy::AdIgnorantiam, // InformalFallacy::AdIgnorantiam,
InformalFallacy::SpecialPleading, // InformalFallacy::SpecialPleading,
InformalFallacy::BeggingTheQuestion, // InformalFallacy::BeggingTheQuestion,
]; // ];
variants.choose(&mut rng).unwrap().clone() // variants.choose(&mut rng).unwrap().clone()
} // }
} }
/// Bias module, which includes various lessons related to cognitive biases and their impact on reasoning. /// Bias module, which includes various lessons related to cognitive biases and their impact on reasoning.
@ -332,18 +330,18 @@ impl Bias {
Bias::BarnumEffect => "barnum_effect".to_string(), Bias::BarnumEffect => "barnum_effect".to_string(),
} }
} }
pub(crate) fn random() -> Self { // pub(crate) fn random() -> Self {
let mut rng = rng(); // let mut rng = rng();
let variants = [ // let variants = [
Bias::ConfirmationBias, // Bias::ConfirmationBias,
Bias::TheHaloEffect, // Bias::TheHaloEffect,
Bias::FundamentalAttributionError, // Bias::FundamentalAttributionError,
Bias::InGroupBias, // Bias::InGroupBias,
Bias::DunningKrugerEffect, // Bias::DunningKrugerEffect,
Bias::BarnumEffect, // Bias::BarnumEffect,
]; // ];
variants.choose(&mut rng).unwrap().clone() // variants.choose(&mut rng).unwrap().clone()
} // }
} }
/// Appraisal module, which includes various lessons related to the evaluation and appraisal of arguments. /// Appraisal module, which includes various lessons related to the evaluation and appraisal of arguments.
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
@ -371,12 +369,12 @@ impl Appraisal {
Appraisal::CounterArgument => "counter_argument".to_string(), Appraisal::CounterArgument => "counter_argument".to_string(),
} }
} }
pub(crate) fn random() -> Self { // pub(crate) fn random() -> Self {
let mut rng = rng(); // let mut rng = rng();
let variants = [ // let variants = [
Appraisal::ConversionToPropositional, // Appraisal::ConversionToPropositional,
Appraisal::CounterArgument, // Appraisal::CounterArgument,
]; // ];
variants.choose(&mut rng).unwrap().clone() // variants.choose(&mut rng).unwrap().clone()
} // }
} }

View File

@ -60,16 +60,16 @@ impl Learner {
clear_console(); clear_console();
let (cols, _) = size().unwrap(); let (cols, _) = size().unwrap();
let dashes = "-".repeat((cols as usize)); let dashes = "-".repeat(cols as usize);
let dots = " ".repeat((cols as usize).saturating_sub(42)); let dots = " ".repeat((cols as usize).saturating_sub(42));
println!("Progress Report for {}:", self.name); println!("Progress Report for {}:", self.name);
println!("{}", dashes); println!("{}", dashes);
println!("{}", format!(" Unit {} | Mastery |", dots).bold()); println!("{}", format!(" Unit {} | Mastery |", dots).bold());
let mut modules = units::Module::iter(); let modules = units::Module::iter();
for module in modules { for module in modules {
if let Some(progress) = self.progress.get(&module) { if let Some(_) = self.progress.get(&module) {
self.progress.get(&module).unwrap().print_progress(); self.progress.get(&module).unwrap().print_progress();
} }
} }

View File

@ -2,7 +2,7 @@ use std::{fs::File, io::Read};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use crate::{course::story, learner::profile::{self, Learner}}; use crate::{course::story, learner::profile::Learner};
// pub fn save_learner_progress(learner: &Learner, file: &str) -> Result<(), std::io::Error> { // pub fn save_learner_progress(learner: &Learner, file: &str) -> Result<(), std::io::Error> {
// learner.clone().save_to_file(file)?; // learner.clone().save_to_file(file)?;

View File

@ -69,7 +69,7 @@ const OFFENSIVE_TERMS: [&str; 20] = [
"jew", "slut", "tit", "phag" "jew", "slut", "tit", "phag"
]; ];
const affirmations: [&str; 8] = [ const AFFIRMATIONS: [&str; 26] = [
"You're doing great!", "You're doing great!",
"Keep up the good work!", "Keep up the good work!",
"Fantastic effort!", "Fantastic effort!",
@ -78,12 +78,30 @@ const affirmations: [&str; 8] = [
"You're making progress!", "You're making progress!",
"Keep it up!", "Keep it up!",
"Great work!", "Great work!",
"You're unstoppable!",
"Amazing progress!",
"You're crushing it!",
"Keep shining!",
"You're a star!",
"Outstanding work!",
"You're making a difference!",
"You're an inspiration!",
"You're reaching for the stars!",
"Keep shining like a supernova!",
"You're out of this world!",
"You're orbiting success!",
"You're a cosmic force!",
"You're a shooting star!",
"You're exploring new galaxies!",
"You're a stellar achiever!",
"You're blazing a trail through the cosmos!",
"You're a beacon in the universe!",
]; ];
pub fn random_affirmation() -> String { pub fn random_affirmation() -> String {
let mut rng = rand::rng(); let mut rng = rand::rng();
let index = rng.random_range(0..affirmations.len()); let index = rng.random_range(0..AFFIRMATIONS.len());
affirmations[index].to_string() AFFIRMATIONS[index].to_string()
} }
pub fn generate_name() -> String { pub fn generate_name() -> String {
@ -109,15 +127,15 @@ pub fn generate_name() -> String {
} }
pub fn random_unit_intro() -> String { // pub fn random_unit_intro() -> String {
let mut rng = rand::rng(); // let mut rng = rand::rng();
let unit_intros: [&str; 5] = [ // let unit_intros: [&str; 5] = [
"It's time to learn about", // "It's time to learn about",
"Let's dive into", // "Let's dive into",
"Get ready to explore", // "Get ready to explore",
"Prepare to discover", // "Prepare to discover",
"Let's embark on a journey to learn about", // "Let's embark on a journey to learn about",
]; // ];
let index = rng.random_range(0..unit_intros.len()); // let index = rng.random_range(0..unit_intros.len());
unit_intros[index].to_string() // unit_intros[index].to_string()
} // }

View File

@ -1,14 +1,12 @@
use crossterm::{execute, style::Stylize, terminal::{size, Clear, ClearType}}; use crossterm::{execute, style::Stylize, terminal::{size, Clear, ClearType}};
use std::io::stdout; use std::io::stdout;
use super::generators; // const DELAY_SECONDS: u64 = 1;
const DELAY_SECONDS: u64 = 1;
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum StatementType { pub enum StatementType {
Default, Default,
DelayExplanation, // DelayExplanation,
Question, Question,
GeneralFeedback, GeneralFeedback,
CorrectFeedback, CorrectFeedback,
@ -160,10 +158,6 @@ pub fn print_boxed(instruction: &str, statement_type: StatementType) {
StatementType::Question => { StatementType::Question => {
return; return;
} }
StatementType::DelayExplanation => {
wait_for_input_delay(DELAY_SECONDS);
print_boxed(&instruction, StatementType::Default);
}
_ => { _ => {
wait_for_input(); wait_for_input();
} }
@ -171,29 +165,29 @@ pub fn print_boxed(instruction: &str, statement_type: StatementType) {
} }
/// Prints a full-width ruler of designated characters /// Prints a full-width ruler of designated characters
pub fn print_rule(character: char, padding: (bool, bool)) { // pub fn print_rule(character: char, padding: (bool, bool)) {
let (cols, _) = size().unwrap(); // let (cols, _) = size().unwrap();
println!( // println!(
"{}{}{}", // "{}{}{}",
if padding.0 { "\n" } else { "" }, // if padding.0 { "\n" } else { "" },
character.to_string().repeat(cols as usize), // character.to_string().repeat(cols as usize),
if padding.1 { "\n" } else { "" } // if padding.1 { "\n" } else { "" }
); // );
} // }
/// Horizontally center the desired text on the screen /// Horizontally center the desired text on the screen
pub fn print_centered(text: &str) { // pub fn print_centered(text: &str) {
let (cols, _) = size().unwrap(); // let (cols, _) = size().unwrap();
let text_width = text.len() as u16; // let text_width = text.len() as u16;
let start_col = (cols.saturating_sub(text_width)) / 2; // let start_col = (cols.saturating_sub(text_width)) / 2;
println!( // println!(
"\x1B[{}C{}", // "\x1B[{}C{}",
start_col, // start_col,
text // text
); // );
} // }
/// Wait for input from the user /// Wait for input from the user
pub fn wait_for_input() { pub fn wait_for_input() {
@ -213,51 +207,51 @@ pub fn wait_for_input() {
} }
/// Wait for input from the user /// Wait for input from the user
pub fn print_title(title: &str) { // pub fn print_title(title: &str) {
let (cols, _) = size().unwrap(); // let (cols, _) = size().unwrap();
let text_width = title.len() as u16; // let text_width = title.len() as u16;
let start_col = (cols.saturating_sub(text_width)) / 2 + 1; // let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
println!( // println!(
"\x1B[{}C{}", // "\x1B[{}C{}",
start_col, // start_col,
title.bold() // title.bold()
); // );
wait_for_input(); // wait_for_input();
} // }
/// Wait for input from the user /// Wait for input from the user
pub fn wait_for_input_delay(seconds: u64) { // pub fn wait_for_input_delay(seconds: u64) {
let prompt = format!("Remember to be mindful. Advance in {} seconds.", seconds); // let prompt = format!("Remember to be mindful. Advance in {} seconds.", seconds);
let (cols, _) = size().unwrap(); // let (cols, _) = size().unwrap();
let text_width = prompt.len() as u16; // let text_width = prompt.len() as u16;
let start_col = (cols.saturating_sub(text_width)) / 2 + 1; // let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
println!( // println!(
"\x1B[{}C{}", // "\x1B[{}C{}",
start_col, // start_col,
prompt.italic().dark_blue() // prompt.italic().dark_blue()
); // );
std::thread::sleep(std::time::Duration::from_secs(DELAY_SECONDS)); // std::thread::sleep(std::time::Duration::from_secs(DELAY_SECONDS));
} // }
/// Wait for input from the user /// Wait for input from the user
pub fn inform_delay(seconds: u64) { // pub fn inform_delay(seconds: u64) {
let prompt = format!("Remember to be mindful. Answers appear in {} seconds.", seconds); // let prompt = format!("Remember to be mindful. Answers appear in {} seconds.", seconds);
let (cols, _) = size().unwrap(); // let (cols, _) = size().unwrap();
let text_width = prompt.len() as u16; // let text_width = prompt.len() as u16;
let start_col = (cols.saturating_sub(text_width)) / 2 + 1; // let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
println!( // println!(
"\x1B[{}C{}", // "\x1B[{}C{}",
start_col, // start_col,
prompt.italic().dark_blue() // prompt.italic().dark_blue()
); // );
} // }
pub fn unit_screen(title: &str) { pub fn unit_screen(title: &str) {
clear_console(); clear_console();

View File

@ -1,16 +1,16 @@
use inquire::{MultiSelect, Select, Text}; use inquire::{MultiSelect, Select, Text};
use crossterm::terminal; use crossterm::terminal;
use super::printers::{clear_console, inform_delay, print_boxed, StatementType}; use super::printers::{clear_console, print_boxed, StatementType};
const DELAY_SECONDS: u64 = 1; // const DELAY_SECONDS: u64 = 1;
pub fn ask_mcq_delayed(question: &str, options: &[&str]) -> Option<String> { // pub fn ask_mcq_delayed(question: &str, options: &[&str]) -> Option<String> {
print_boxed(question, StatementType::Question); // print_boxed(question, StatementType::Question);
inform_delay(DELAY_SECONDS); // inform_delay(DELAY_SECONDS);
std::thread::sleep(std::time::Duration::from_secs(DELAY_SECONDS)); // std::thread::sleep(std::time::Duration::from_secs(DELAY_SECONDS));
ask_mcq(question, options) // ask_mcq(question, options)
} // }
/// Print a quiz question in an attractive format and get the user's choice /// Print a quiz question in an attractive format and get the user's choice
pub fn ask_mcq(question: &str, options: &[&str]) -> Option<String> { pub fn ask_mcq(question: &str, options: &[&str]) -> Option<String> {