arct_tui/panels/
lesson.rs

1//! Lesson panel - displays interactive lessons and tracks progress
2
3use crate::icons;
4use crate::theme::Theme;
5use arct_core::{Lesson, LessonStep, StepType, ValidationResult, LessonValidator};
6use ratatui::{
7    layout::Rect,
8    style::Style,
9    text::{Line, Span},
10    widgets::{Block, Borders, Paragraph, Wrap},
11    Frame,
12};
13
14/// Lesson panel state
15pub struct LessonPanel {
16    pub current_lesson: Option<Lesson>,
17    current_step_index: usize,
18    user_input: String,
19    validator: LessonValidator,
20    last_validation: Option<ValidationResult>,
21    completed_steps: Vec<usize>,
22}
23
24impl LessonPanel {
25    pub fn new() -> Self {
26        Self {
27            current_lesson: None,
28            current_step_index: 0,
29            user_input: String::new(),
30            validator: LessonValidator::new(),
31            last_validation: None,
32            completed_steps: Vec::new(),
33        }
34    }
35
36    /// Load a lesson
37    pub fn load_lesson(&mut self, lesson: Lesson) {
38        self.current_lesson = Some(lesson);
39        self.current_step_index = 0;
40        self.user_input.clear();
41        self.last_validation = None;
42        self.completed_steps.clear();
43    }
44
45    /// Get current step
46    fn current_step(&self) -> Option<&LessonStep> {
47        self.current_lesson
48            .as_ref()
49            .and_then(|lesson| lesson.steps.get(self.current_step_index))
50    }
51
52    /// Check if user input is valid for current step
53    pub fn validate_current_step(&mut self, input: &str) -> ValidationResult {
54        if let Some(step) = self.current_step() {
55            let result = match &step.step_type {
56                StepType::CommandExercise {
57                    expected_command,
58                    validation,
59                    success_message,
60                } => {
61                    let validation_result =
62                        self.validator.validate_command(input, expected_command, validation);
63
64                    if validation_result.is_success() {
65                        ValidationResult::Success {
66                            message: success_message.clone(),
67                        }
68                    } else {
69                        validation_result
70                    }
71                }
72                StepType::MultipleChoice {
73                    correct_index, ..
74                } => {
75                    if let Ok(choice) = input.parse::<usize>() {
76                        self.validator.validate_multiple_choice(choice, *correct_index)
77                    } else {
78                        ValidationResult::Failure {
79                            message: "Please enter a number.".to_string(),
80                            hint: None,
81                        }
82                    }
83                }
84                StepType::Information { .. } => {
85                    // Information steps just need any key press to continue
86                    ValidationResult::Success {
87                        message: "Continue to next step.".to_string(),
88                    }
89                }
90                _ => ValidationResult::Success {
91                    message: "Continue.".to_string(),
92                },
93            };
94
95            self.last_validation = Some(result.clone());
96            result
97        } else {
98            ValidationResult::Failure {
99                message: "No active step.".to_string(),
100                hint: None,
101            }
102        }
103    }
104
105    /// Move to next step
106    pub fn next_step(&mut self) -> bool {
107        if let Some(lesson) = &self.current_lesson {
108            if !self.completed_steps.contains(&self.current_step_index) {
109                self.completed_steps.push(self.current_step_index);
110            }
111
112            if self.current_step_index + 1 < lesson.steps.len() {
113                self.current_step_index += 1;
114                self.user_input.clear();
115                self.last_validation = None;
116                true
117            } else {
118                false // Lesson complete
119            }
120        } else {
121            false
122        }
123    }
124
125    /// Move to previous step
126    pub fn previous_step(&mut self) {
127        if self.current_step_index > 0 {
128            self.current_step_index -= 1;
129            self.user_input.clear();
130            self.last_validation = None;
131        }
132    }
133
134    /// Get completion percentage
135    pub fn completion_percentage(&self) -> f32 {
136        if let Some(lesson) = &self.current_lesson {
137            let total = lesson.steps.len();
138            if total == 0 {
139                return 0.0;
140            }
141            (self.completed_steps.len() as f32 / total as f32) * 100.0
142        } else {
143            0.0
144        }
145    }
146
147    /// Render the lesson panel
148    pub fn render(&self, frame: &mut Frame, area: Rect, focused: bool, theme: &Theme) {
149        let border_style = if focused {
150            theme.style_border_focused()
151        } else {
152            theme.style_border()
153        };
154
155        if let Some(lesson) = &self.current_lesson {
156            // Render current step (includes header info in title)
157            if let Some(step) = self.current_step() {
158                self.render_step(frame, area, lesson, step, theme, border_style);
159            }
160        } else {
161            // No lesson loaded - show lesson selection screen
162            self.render_lesson_selection(frame, area, theme, border_style);
163        }
164    }
165
166    fn render_step(
167        &self,
168        frame: &mut Frame,
169        area: Rect,
170        lesson: &Lesson,
171        step: &LessonStep,
172        theme: &Theme,
173        border_style: Style,
174    ) {
175        let mut lines = Vec::new();
176
177        // Step title - always show
178        lines.push(Line::from(vec![
179            Span::styled(format!("Step {}: ", step.step_number), theme.style_accent()),
180            Span::styled(&step.title, theme.style_header()),
181        ]));
182
183        // Render based on step type
184        match &step.step_type {
185            StepType::CommandExercise { .. } => {
186                // HOW TO instruction
187                lines.push(Line::from(vec![
188                    Span::styled("▶ Type command in Shell → Enter", theme.style_warning()),
189                ]));
190                // Task instruction
191                if !step.instruction.is_empty() {
192                    lines.push(Line::from(vec![
193                        Span::styled("Task: ", theme.style_accent()),
194                        Span::styled(&step.instruction, theme.style_normal()),
195                    ]));
196                }
197                // Hint
198                if let Some(hint) = &step.hint {
199                    lines.push(Line::from(vec![
200                        icons::hint(),
201                        Span::styled(hint, theme.style_dim()),
202                    ]));
203                }
204                // Validation result
205                if let Some(validation) = &self.last_validation {
206                    match validation {
207                        ValidationResult::Success { message } => {
208                            lines.push(Line::from(vec![
209                                icons::success(),
210                                Span::styled(message, theme.style_success()),
211                            ]));
212                        }
213                        ValidationResult::Failure { message, hint } => {
214                            lines.push(Line::from(vec![
215                                icons::error(),
216                                Span::styled(message, theme.style_error()),
217                            ]));
218                            if let Some(h) = hint {
219                                lines.push(Line::from(vec![
220                                    icons::hint(),
221                                    Span::styled(h, theme.style_dim()),
222                                ]));
223                            }
224                        }
225                        ValidationResult::Partial { message, progress } => {
226                            lines.push(Line::from(vec![
227                                icons::warning(),
228                                Span::styled(
229                                    format!("{} ({:.0}%)", message, progress),
230                                    theme.style_warning(),
231                                ),
232                            ]));
233                        }
234                    }
235                }
236            }
237            StepType::MultipleChoice {
238                question,
239                options,
240                explanation,
241                ..
242            } => {
243                // HOW TO + Question combined
244                lines.push(Line::from(vec![
245                    Span::styled(format!("▶ Type 0-{} → ", options.len() - 1), theme.style_warning()),
246                    icons::question(),
247                    Span::styled(question, theme.style_normal()),
248                ]));
249                // Options
250                for (i, option) in options.iter().enumerate() {
251                    lines.push(Line::from(vec![
252                        Span::styled(format!("  {}. ", i), theme.style_accent()),
253                        Span::styled(option, theme.style_normal()),
254                    ]));
255                }
256                // Show explanation if answered correctly
257                if let Some(ValidationResult::Success { .. }) = &self.last_validation {
258                    lines.push(Line::from(vec![
259                        icons::success(),
260                        Span::styled(explanation, theme.style_success()),
261                    ]));
262                }
263            }
264            StepType::Information { content } => {
265                // HOW TO
266                lines.push(Line::from(vec![
267                    Span::styled("▶ Press Enter to continue", theme.style_warning()),
268                ]));
269                // Display information
270                for line in content.lines() {
271                    lines.push(Line::from(vec![Span::styled(line, theme.style_normal())]));
272                }
273            }
274            StepType::FillInBlank { template, .. } => {
275                // HOW TO
276                lines.push(Line::from(vec![
277                    Span::styled("▶ Fill blank, type in Shell → Enter", theme.style_warning()),
278                ]));
279                lines.push(Line::from(vec![
280                    icons::note(),
281                    Span::styled(&step.instruction, theme.style_normal()),
282                ]));
283                lines.push(Line::from(vec![
284                    Span::styled("Template: ", theme.style_accent()),
285                    Span::styled(template, theme.style_dim()),
286                ]));
287            }
288            StepType::Practice { goal, hints, .. } => {
289                // HOW TO + Goal combined
290                lines.push(Line::from(vec![
291                    Span::styled("▶ Try commands → ", theme.style_warning()),
292                    icons::target(),
293                    Span::styled(goal, theme.style_normal()),
294                ]));
295                // Hints inline
296                if !hints.is_empty() {
297                    for hint in hints {
298                        lines.push(Line::from(vec![
299                            icons::hint(),
300                            Span::styled(hint, theme.style_dim()),
301                        ]));
302                    }
303                }
304            }
305        }
306
307        // Build title with progress info (compact)
308        let progress = self.completion_percentage();
309        let title = format!(
310            " {} {}/{} | {:.0}% ",
311            lesson.title,
312            self.current_step_index + 1,
313            lesson.steps.len(),
314            progress
315        );
316
317        let block = Block::default()
318            .title(title)
319            .borders(Borders::ALL)
320            .border_style(border_style)
321            .style(theme.style_block());
322
323        let paragraph = Paragraph::new(lines)
324            .block(block)
325            .wrap(Wrap { trim: false });
326        frame.render_widget(paragraph, area);
327    }
328
329    fn render_lesson_selection(
330        &self,
331        frame: &mut Frame,
332        area: Rect,
333        theme: &Theme,
334        border_style: Style,
335    ) {
336        let block = Block::default()
337            .title(format!(" {}Interactive Lessons ", icons::lesson().content))
338            .borders(Borders::ALL)
339            .border_style(border_style)
340            .style(theme.style_block());  // Set background for light themes
341
342        let paragraph = Paragraph::new(vec![
343            Line::from(""),
344            Line::from(vec![
345                icons::welcome(),
346                Span::styled("Welcome to Interactive Lessons!", theme.style_accent()),
347            ]),
348            Line::from(""),
349            Line::from(vec![
350                Span::styled("🎓 ", theme.style_accent()),
351                Span::styled("10 comprehensive lessons", theme.style_normal()),
352                Span::styled(" available", theme.style_dim()),
353            ]),
354            Line::from(vec![
355                Span::styled("🏆 ", theme.style_accent()),
356                Span::styled("Track progress & earn achievements", theme.style_normal()),
357            ]),
358            Line::from(vec![
359                Span::styled("🛡️  ", theme.style_accent()),
360                Span::styled("Safe virtual filesystem", theme.style_normal()),
361                Span::styled(" for hands-on practice", theme.style_dim()),
362            ]),
363            Line::from(""),
364            Line::from(""),
365            Line::from(vec![
366                Span::styled("📚 Select a lesson:", theme.style_header()),
367            ]),
368            Line::from(""),
369            Line::from(vec![
370                Span::styled("  Press ", theme.style_dim()),
371                Span::styled("m", theme.style_accent()),
372                Span::styled(" to open the lesson menu", theme.style_dim()),
373            ]),
374            Line::from(vec![
375                Span::styled("  Use ", theme.style_dim()),
376                Span::styled("↑/↓", theme.style_accent()),
377                Span::styled(" or ", theme.style_dim()),
378                Span::styled("1-9,0", theme.style_accent()),
379                Span::styled(" to select", theme.style_dim()),
380            ]),
381            Line::from(vec![
382                Span::styled("  Press ", theme.style_dim()),
383                Span::styled("Enter", theme.style_accent()),
384                Span::styled(" to start learning!", theme.style_dim()),
385            ]),
386            Line::from(""),
387            Line::from(""),
388            Line::from(vec![
389                Span::styled("💡 Tip: ", theme.style_accent()),
390                Span::styled("Complete lessons to unlock achievements", theme.style_dim()),
391            ]),
392            Line::from(vec![
393                Span::styled("       and build your learning streak!", theme.style_dim()),
394            ]),
395        ])
396        .block(block)
397        .wrap(Wrap { trim: false });
398
399        frame.render_widget(paragraph, area);
400    }
401}
402
403impl Default for LessonPanel {
404    fn default() -> Self {
405        Self::new()
406    }
407}