logicaffeine_web/
struggle.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum StruggleReason {
10 Inactivity,
11 WrongAttempt,
12}
13
14impl StruggleReason {
15 pub fn message(&self) -> &'static str {
17 match self {
18 StruggleReason::Inactivity => "Taking your time? Here's a hint to help you along.",
19 StruggleReason::WrongAttempt => "Not quite! Let me help you think through this.",
20 }
21 }
22}
23
24#[derive(Debug, Clone, Copy)]
26pub struct StruggleConfig {
27 pub inactivity_threshold_secs: u64,
29 pub wrong_attempt_threshold: u32,
31}
32
33impl Default for StruggleConfig {
34 fn default() -> Self {
35 Self {
36 inactivity_threshold_secs: 5,
37 wrong_attempt_threshold: 1,
38 }
39 }
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct StruggleDetector {
45 pub config: StruggleConfig,
46 pub is_struggling: bool,
47 pub reason: Option<StruggleReason>,
48 pub wrong_attempts: u32,
49 pub inactivity_triggered: bool,
50}
51
52impl StruggleDetector {
53 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn with_config(config: StruggleConfig) -> Self {
58 Self {
59 config,
60 ..Default::default()
61 }
62 }
63
64 pub fn record_wrong_attempt(&mut self) {
66 self.wrong_attempts += 1;
67 if self.wrong_attempts >= self.config.wrong_attempt_threshold {
68 self.is_struggling = true;
69 self.reason = Some(StruggleReason::WrongAttempt);
70 }
71 }
72
73 pub fn record_correct_attempt(&mut self) {
75 self.is_struggling = false;
77 self.inactivity_triggered = false;
78 }
79
80 pub fn record_activity(&mut self) {
82 self.inactivity_triggered = false;
84 }
85
86 pub fn trigger_inactivity(&mut self) {
88 if !self.inactivity_triggered {
89 self.inactivity_triggered = true;
90 self.is_struggling = true;
91 if self.reason.is_none() {
93 self.reason = Some(StruggleReason::Inactivity);
94 }
95 }
96 }
97
98 pub fn reset(&mut self) {
100 self.is_struggling = false;
101 self.reason = None;
102 self.wrong_attempts = 0;
103 self.inactivity_triggered = false;
104 }
105
106 pub fn should_show_hints(&self) -> bool {
108 self.is_struggling
109 }
110
111 pub fn reason(&self) -> Option<StruggleReason> {
113 self.reason
114 }
115
116 pub fn struggle_message(&self) -> Option<&'static str> {
118 match self.reason {
119 Some(StruggleReason::Inactivity) => Some("Taking your time? Here's a hint to help you along."),
120 Some(StruggleReason::WrongAttempt) => Some("Not quite! Let me help you think through this."),
121 None => None,
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_no_struggle_initially() {
132 let detector = StruggleDetector::new();
133 assert!(!detector.is_struggling);
134 assert!(detector.reason.is_none());
135 }
136
137 #[test]
138 fn test_struggle_after_5s_inactivity() {
139 let mut detector = StruggleDetector::new();
140 assert!(!detector.is_struggling);
141
142 detector.trigger_inactivity();
143
144 assert!(detector.is_struggling);
145 assert_eq!(detector.reason, Some(StruggleReason::Inactivity));
146 }
147
148 #[test]
149 fn test_struggle_after_wrong_attempt() {
150 let mut detector = StruggleDetector::new();
151 assert!(!detector.is_struggling);
152
153 detector.record_wrong_attempt();
154
155 assert!(detector.is_struggling);
156 assert_eq!(detector.reason, Some(StruggleReason::WrongAttempt));
157 }
158
159 #[test]
160 fn test_reset_clears_struggle() {
161 let mut detector = StruggleDetector::new();
162 detector.record_wrong_attempt();
163 assert!(detector.is_struggling);
164
165 detector.reset();
166
167 assert!(!detector.is_struggling);
168 assert!(detector.reason.is_none());
169 assert_eq!(detector.wrong_attempts, 0);
170 }
171
172 #[test]
173 fn test_configurable_threshold() {
174 let config = StruggleConfig {
175 inactivity_threshold_secs: 10,
176 wrong_attempt_threshold: 2,
177 };
178 let mut detector = StruggleDetector::with_config(config);
179
180 detector.record_wrong_attempt();
182 assert!(!detector.is_struggling);
183
184 detector.record_wrong_attempt();
186 assert!(detector.is_struggling);
187 }
188
189 #[test]
190 fn test_inactivity_only_triggers_once() {
191 let mut detector = StruggleDetector::new();
192
193 detector.trigger_inactivity();
194 assert!(detector.inactivity_triggered);
195
196 detector.reason = None;
198 detector.trigger_inactivity();
199 assert!(detector.reason.is_none()); }
201
202 #[test]
203 fn test_should_show_hints() {
204 let mut detector = StruggleDetector::new();
205 assert!(!detector.should_show_hints());
206
207 detector.record_wrong_attempt();
208 assert!(detector.should_show_hints());
209 }
210
211 #[test]
212 fn test_struggle_message() {
213 let mut detector = StruggleDetector::new();
214 assert!(detector.struggle_message().is_none());
215
216 detector.trigger_inactivity();
217 assert!(detector.struggle_message().is_some());
218 assert!(detector.struggle_message().unwrap().contains("hint"));
219 }
220}