ai_code_buddy/widgets/
analysis.rs

1use bevy::prelude::*;
2use bevy_ratatui::{error::exit_on_error, terminal::RatatuiContext};
3use bevy_tokio_tasks::TokioTasksRuntime;
4use crossterm::event::{KeyCode, KeyEventKind};
5use ratatui::{
6    buffer::Buffer,
7    layout::{Alignment, Constraint, Direction, Layout, Rect},
8    style::Style,
9    text::{Line, Span},
10    widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, StatefulWidgetRef, WidgetRef},
11};
12
13use crate::{
14    args::Args,
15    bevy_states::app::AppState,
16    core,
17    events::{analysis::AnalysisEvent, app::AppEvent},
18    theme::THEME,
19    widget_states::analysis::AnalysisWidgetState,
20};
21
22pub struct AnalysisPlugin;
23
24impl Plugin for AnalysisPlugin {
25    fn build(&self, app: &mut App) {
26        app.add_event::<AnalysisEvent>()
27            .init_resource::<AnalysisWidgetState>()
28            .add_systems(PreUpdate, analysis_event_handler)
29            .add_systems(Update, render_analysis.pipe(exit_on_error));
30    }
31}
32
33pub fn analysis_event_handler(
34    mut analysis_events: EventReader<AnalysisEvent>,
35    mut analysis_state: ResMut<AnalysisWidgetState>,
36    mut app_events: EventWriter<AppEvent>,
37    args: Res<Args>,
38    tokio_runtime: ResMut<TokioTasksRuntime>,
39) {
40    for event in analysis_events.read() {
41        match event {
42            AnalysisEvent::KeyEvent(key_event) => {
43                match key_event.code {
44                    KeyCode::Esc => {
45                        // Always allow going back to overview with Escape
46                        // If analysis is running, this will stop it and go back
47                        if analysis_state.is_analyzing {
48                            analysis_state.is_analyzing = false;
49                        }
50                        app_events.send(AppEvent::SwitchTo(AppState::Overview));
51                    }
52                    _ => {
53                        // Only handle other keys on release to avoid double-triggering
54                        if key_event.kind == KeyEventKind::Release {
55                            match key_event.code {
56                                KeyCode::Enter => {
57                                    if !analysis_state.is_analyzing
58                                        && analysis_state.review.is_none()
59                                    {
60                                        start_analysis(&mut analysis_state, &args, &tokio_runtime);
61                                    }
62                                }
63                                KeyCode::Up => {
64                                    if !analysis_state.is_analyzing {
65                                        analysis_state.move_issue_selection(-1);
66                                    }
67                                }
68                                KeyCode::Down => {
69                                    if !analysis_state.is_analyzing {
70                                        analysis_state.move_issue_selection(1);
71                                    }
72                                }
73                                KeyCode::Char('r') => {
74                                    if !analysis_state.is_analyzing {
75                                        app_events.send(AppEvent::SwitchTo(AppState::Reports));
76                                    }
77                                }
78                                _ => {}
79                            }
80                        }
81                    }
82                }
83            }
84            AnalysisEvent::MouseEvent(_mouse_event) => {
85                // Handle mouse events if needed
86            }
87        }
88    }
89}
90
91fn start_analysis(
92    analysis_state: &mut AnalysisWidgetState,
93    args: &Args,
94    _tokio_runtime: &TokioTasksRuntime,
95) {
96    analysis_state.start_analysis();
97
98    // Perform analysis synchronously to avoid GitAnalyzer Send issues
99    match core::analysis::perform_analysis(args) {
100        Ok(review) => {
101            analysis_state.complete_analysis(review);
102        }
103        Err(e) => {
104            eprintln!("AI analysis failed: {e}");
105            analysis_state.is_analyzing = false;
106        }
107    }
108}
109
110fn render_analysis(
111    app_state: Res<State<AppState>>,
112    mut ratatui_context: ResMut<RatatuiContext>,
113    mut analysis_state: ResMut<AnalysisWidgetState>,
114) -> color_eyre::Result<()> {
115    if app_state.get() != &AppState::Analysis {
116        return Ok(());
117    }
118
119    ratatui_context.draw(|frame| {
120        let area = frame.area();
121        frame.render_stateful_widget_ref(AnalysisWidget, area, &mut analysis_state);
122    })?;
123
124    Ok(())
125}
126
127pub struct AnalysisWidget;
128
129impl StatefulWidgetRef for AnalysisWidget {
130    type State = AnalysisWidgetState;
131
132    fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
133        let chunks = Layout::default()
134            .direction(Direction::Vertical)
135            .constraints([
136                Constraint::Length(3), // Title
137                Constraint::Min(10),   // Content
138                Constraint::Length(3), // Status bar
139            ])
140            .split(area);
141
142        // Render title
143        let title = Paragraph::new("🔍 Code Analysis")
144            .style(THEME.title_style())
145            .alignment(Alignment::Center)
146            .block(
147                Block::default()
148                    .borders(Borders::ALL)
149                    .border_style(THEME.header_style()),
150            );
151        title.render_ref(chunks[0], buf);
152
153        // Render content based on state
154        if state.is_analyzing {
155            self.render_analysis_progress(chunks[1], buf, state);
156        } else if let Some(review) = &state.review {
157            self.render_results(chunks[1], buf, state, review);
158        } else {
159            self.render_start_screen(chunks[1], buf);
160        }
161
162        // Render status bar
163        self.render_status_bar(chunks[2], buf, state);
164    }
165}
166
167impl AnalysisWidget {
168    fn render_start_screen(&self, area: Rect, buf: &mut Buffer) {
169        let content = Paragraph::new(vec![
170            Line::from(""),
171            Line::from("Press Enter to start the code analysis"),
172            Line::from(""),
173            Line::from("This will analyze your Git repository for:"),
174            Line::from("â€ĸ Security vulnerabilities"),
175            Line::from("â€ĸ Performance issues"),
176            Line::from("â€ĸ Code quality problems"),
177            Line::from("â€ĸ Best practice violations"),
178        ])
179        .alignment(Alignment::Center)
180        .block(
181            Block::default()
182                .borders(Borders::ALL)
183                .title("Ready to Analyze")
184                .title_style(THEME.header_style()),
185        );
186
187        content.render_ref(area, buf);
188    }
189
190    fn render_analysis_progress(&self, area: Rect, buf: &mut Buffer, state: &AnalysisWidgetState) {
191        let chunks = Layout::default()
192            .direction(Direction::Vertical)
193            .constraints([
194                Constraint::Length(5), // Progress bar
195                Constraint::Min(3),    // Current file
196            ])
197            .split(area);
198
199        // Progress bar
200        let progress = Gauge::default()
201            .block(
202                Block::default()
203                    .borders(Borders::ALL)
204                    .title("Analysis Progress")
205                    .title_style(THEME.header_style()),
206            )
207            .gauge_style(THEME.success_style())
208            .percent(state.progress as u16)
209            .label(format!("{:.1}%", state.progress));
210
211        progress.render_ref(chunks[0], buf);
212
213        // Current file
214        let current_file = Paragraph::new(vec![
215            Line::from(""),
216            Line::from(vec![
217                Span::styled("Currently analyzing: ", THEME.info_style()),
218                Span::raw(&state.current_file),
219            ]),
220        ])
221        .alignment(Alignment::Center)
222        .block(
223            Block::default()
224                .borders(Borders::ALL)
225                .title("Status")
226                .title_style(THEME.header_style()),
227        );
228
229        current_file.render_ref(chunks[1], buf);
230    }
231
232    fn render_results(
233        &self,
234        area: Rect,
235        buf: &mut Buffer,
236        state: &AnalysisWidgetState,
237        review: &crate::core::review::Review,
238    ) {
239        let chunks = Layout::default()
240            .direction(Direction::Horizontal)
241            .constraints([
242                Constraint::Percentage(30), // Summary
243                Constraint::Percentage(70), // Issue list
244            ])
245            .split(area);
246
247        // Summary
248        self.render_summary(chunks[0], buf, review);
249
250        // Issue list
251        self.render_issue_list(chunks[1], buf, state, review);
252    }
253
254    fn render_summary(&self, area: Rect, buf: &mut Buffer, review: &crate::core::review::Review) {
255        let summary_lines = vec![
256            Line::from(""),
257            Line::from(vec![
258                Span::styled("📁 Files: ", THEME.info_style()),
259                Span::raw(format!("{}", review.files_count)),
260            ]),
261            Line::from(""),
262            Line::from(vec![
263                Span::styled("🐛 Total Issues: ", THEME.info_style()),
264                Span::raw(format!("{}", review.issues_count)),
265            ]),
266            Line::from(""),
267            Line::from(vec![
268                Span::styled("🚨 Critical: ", THEME.error_style()),
269                Span::raw(format!("{}", review.critical_issues)),
270            ]),
271            Line::from(vec![
272                Span::styled("âš ī¸  High: ", THEME.warning_style()),
273                Span::raw(format!("{}", review.high_issues)),
274            ]),
275            Line::from(vec![
276                Span::styled("đŸ”ļ Medium: ", THEME.warning_style()),
277                Span::raw(format!("{}", review.medium_issues)),
278            ]),
279            Line::from(vec![
280                Span::styled("â„šī¸  Low: ", THEME.info_style()),
281                Span::raw(format!("{}", review.low_issues)),
282            ]),
283        ];
284
285        let summary = Paragraph::new(summary_lines).block(
286            Block::default()
287                .borders(Borders::ALL)
288                .title("Summary")
289                .title_style(THEME.header_style()),
290        );
291
292        summary.render_ref(area, buf);
293    }
294
295    fn render_issue_list(
296        &self,
297        area: Rect,
298        buf: &mut Buffer,
299        state: &AnalysisWidgetState,
300        review: &crate::core::review::Review,
301    ) {
302        if review.issues.is_empty() {
303            let no_issues = Paragraph::new(vec![
304                Line::from(""),
305                Line::from("🎉 No issues found!"),
306                Line::from(""),
307                Line::from("Your code looks clean. Great job!"),
308            ])
309            .alignment(Alignment::Center)
310            .block(
311                Block::default()
312                    .borders(Borders::ALL)
313                    .title("Issues")
314                    .title_style(THEME.header_style()),
315            );
316            no_issues.render_ref(area, buf);
317            return;
318        }
319
320        let items: Vec<ListItem> = review
321            .issues
322            .iter()
323            .enumerate()
324            .map(|(i, issue)| {
325                let severity_icon = match issue.severity.as_str() {
326                    "Critical" => "🚨",
327                    "High" => "âš ī¸",
328                    "Medium" => "đŸ”ļ",
329                    "Low" => "â„šī¸",
330                    _ => "💡",
331                };
332
333                let severity_style = match issue.severity.as_str() {
334                    "Critical" => THEME.error_style(),
335                    "High" => THEME.warning_style(),
336                    "Medium" => THEME.warning_style(),
337                    "Low" => THEME.info_style(),
338                    _ => Style::default(),
339                };
340
341                let is_selected = i == state.selected_issue;
342
343                // Create a multi-line item for better readability
344                let lines = vec![
345                    Line::from(vec![
346                        Span::styled(format!("{severity_icon} "), severity_style),
347                        Span::styled(issue.severity.to_string(), severity_style),
348                        Span::raw("  "),
349                        Span::styled(format!("{}:{}", issue.file, issue.line), THEME.info_style()),
350                    ]),
351                    Line::from(vec![
352                        Span::raw("   "),
353                        Span::styled(format!("{}: ", issue.category), THEME.header_style()),
354                        Span::raw(issue.description.to_string()),
355                    ]),
356                    Line::from(""), // Empty line for spacing
357                ];
358
359                let style = if is_selected {
360                    THEME.selected_style()
361                } else {
362                    Style::default()
363                };
364
365                ListItem::new(lines).style(style)
366            })
367            .collect();
368
369        let issue_list = List::new(items)
370            .block(
371                Block::default()
372                    .borders(Borders::ALL)
373                    .title(format!(
374                        "Issues ({}/{})",
375                        state.selected_issue + 1,
376                        review.issues.len().max(1)
377                    ))
378                    .title_style(THEME.header_style()),
379            )
380            .highlight_style(THEME.selected_style());
381
382        WidgetRef::render_ref(&issue_list, area, buf);
383    }
384
385    fn render_status_bar(&self, area: Rect, buf: &mut Buffer, state: &AnalysisWidgetState) {
386        let status_text = if state.is_analyzing {
387            "Analysis in progress... Please wait"
388        } else if state.review.is_some() {
389            "Use ↑↓ to navigate issues, R for reports, Esc to go back"
390        } else {
391            "Enter to start analysis, Esc to go back"
392        };
393
394        let status = Paragraph::new(status_text)
395            .style(THEME.info_style())
396            .alignment(Alignment::Center)
397            .block(
398                Block::default()
399                    .borders(Borders::TOP)
400                    .border_style(THEME.info_style()),
401            );
402
403        status.render_ref(area, buf);
404    }
405}