1use 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
14pub 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 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 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 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 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 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 }
120 } else {
121 false
122 }
123 }
124
125 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 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 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 if let Some(step) = self.current_step() {
158 self.render_step(frame, area, lesson, step, theme, border_style);
159 }
160 } else {
161 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 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 match &step.step_type {
185 StepType::CommandExercise { .. } => {
186 lines.push(Line::from(vec![
188 Span::styled("▶ Type command in Shell → Enter", theme.style_warning()),
189 ]));
190 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 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 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 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 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 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 lines.push(Line::from(vec![
267 Span::styled("▶ Press Enter to continue", theme.style_warning()),
268 ]));
269 for line in content.lines() {
271 lines.push(Line::from(vec![Span::styled(line, theme.style_normal())]));
272 }
273 }
274 StepType::FillInBlank { template, .. } => {
275 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 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 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 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()); 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}