ai_code_buddy/widgets/
reports.rs

1use bevy::prelude::*;
2use bevy_ratatui::{error::exit_on_error, terminal::RatatuiContext};
3use crossterm::event::{KeyCode, KeyEventKind};
4use ratatui::{
5    buffer::Buffer,
6    layout::{Alignment, Constraint, Direction, Layout, Rect},
7    style::Style,
8    text::{Line, Span},
9    widgets::{Block, Borders, List, ListItem, Paragraph, StatefulWidgetRef, WidgetRef},
10};
11
12use crate::{
13    bevy_states::app::AppState,
14    events::{app::AppEvent, reports::ReportsEvent},
15    theme::THEME,
16    widget_states::{
17        analysis::AnalysisWidgetState,
18        reports::{ExportStatus, ReportFormat, ReportsWidgetState, ViewMode},
19    },
20};
21
22pub struct ReportsPlugin;
23
24impl Plugin for ReportsPlugin {
25    fn build(&self, app: &mut App) {
26        app.add_event::<ReportsEvent>()
27            .init_resource::<ReportsWidgetState>()
28            .add_systems(PreUpdate, reports_event_handler)
29            .add_systems(Update, sync_analysis_data)
30            .add_systems(Update, render_reports.pipe(exit_on_error));
31    }
32}
33
34fn sync_analysis_data(
35    analysis_state: Res<AnalysisWidgetState>,
36    mut reports_state: ResMut<ReportsWidgetState>,
37) {
38    // Sync review data from analysis to reports
39    if let Some(review) = &analysis_state.review {
40        if reports_state.review.is_none() {
41            reports_state.set_review(review.clone());
42        }
43    }
44}
45
46fn reports_event_handler(
47    mut reports_events: EventReader<ReportsEvent>,
48    mut reports_state: ResMut<ReportsWidgetState>,
49    mut app_events: EventWriter<AppEvent>,
50) {
51    for event in reports_events.read() {
52        match event {
53            ReportsEvent::KeyEvent(key_event) => {
54                match key_event.code {
55                    KeyCode::Esc => {
56                        // Handle escape based on current view mode
57                        match reports_state.view_mode {
58                            ViewMode::Report => {
59                                // Go back to selection view
60                                reports_state.back_to_selection();
61                            }
62                            ViewMode::Selection => {
63                                // Go back to overview
64                                app_events.send(AppEvent::SwitchTo(AppState::Overview));
65                            }
66                        }
67                    }
68                    _ => {
69                        // Only handle other keys on release to avoid double-triggering
70                        if key_event.kind == KeyEventKind::Release {
71                            match key_event.code {
72                                KeyCode::Left => {
73                                    reports_state.previous_format();
74                                }
75                                KeyCode::Right => {
76                                    reports_state.next_format();
77                                }
78                                KeyCode::Tab => {
79                                    reports_state.next_format();
80                                }
81                                KeyCode::Enter => {
82                                    match reports_state.view_mode {
83                                        ViewMode::Selection => {
84                                            // Generate and show the report
85                                            reports_state.generate_report();
86                                        }
87                                        ViewMode::Report => {
88                                            // Export the current report
89                                            export_report(&mut reports_state);
90                                        }
91                                    }
92                                }
93                                KeyCode::Char('a') => {
94                                    app_events.send(AppEvent::SwitchTo(AppState::Analysis));
95                                }
96                                _ => {}
97                            }
98                        }
99                    }
100                }
101            }
102            ReportsEvent::MouseEvent(_mouse_event) => {
103                // Handle mouse events if needed
104            }
105        }
106    }
107}
108
109fn export_report(reports_state: &mut ReportsWidgetState) {
110    if let Some(_review) = &reports_state.review {
111        let format = match reports_state.selected_format {
112            ReportFormat::Summary => "summary".to_string(),
113            ReportFormat::Detailed => "detailed".to_string(),
114            ReportFormat::Json => "json".to_string(),
115            ReportFormat::Markdown => "markdown".to_string(),
116        };
117
118        reports_state.start_export(format.clone());
119
120        // TODO: Implement actual file export
121        let filename = format!(
122            "code_review_report.{}",
123            match reports_state.selected_format {
124                ReportFormat::Json => "json",
125                ReportFormat::Markdown => "md",
126                _ => "txt",
127            }
128        );
129
130        reports_state.complete_export(filename);
131    }
132}
133
134fn render_reports(
135    app_state: Res<State<AppState>>,
136    mut ratatui_context: ResMut<RatatuiContext>,
137    mut reports_state: ResMut<ReportsWidgetState>,
138) -> color_eyre::Result<()> {
139    if app_state.get() != &AppState::Reports {
140        return Ok(());
141    }
142
143    ratatui_context.draw(|frame| {
144        let area = frame.area();
145        frame.render_stateful_widget_ref(ReportsWidget, area, &mut reports_state);
146    })?;
147
148    Ok(())
149}
150
151struct ReportsWidget;
152
153impl StatefulWidgetRef for ReportsWidget {
154    type State = ReportsWidgetState;
155
156    fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
157        let chunks = Layout::default()
158            .direction(Direction::Vertical)
159            .constraints([
160                Constraint::Length(3), // Title
161                Constraint::Min(10),   // Content
162                Constraint::Length(3), // Status bar
163            ])
164            .split(area);
165
166        // Render title
167        let title_text = match state.view_mode {
168            ViewMode::Selection => "📊 Reports & Export",
169            ViewMode::Report => "📄 Generated Report",
170        };
171
172        let title = Paragraph::new(title_text)
173            .style(THEME.title_style())
174            .alignment(Alignment::Center)
175            .block(
176                Block::default()
177                    .borders(Borders::ALL)
178                    .border_style(THEME.header_style()),
179            );
180        title.render_ref(chunks[0], buf);
181
182        // Render content based on view mode
183        match state.view_mode {
184            ViewMode::Selection => {
185                if state.review.is_some() {
186                    self.render_report_content(chunks[1], buf, state);
187                } else {
188                    self.render_no_data(chunks[1], buf);
189                }
190            }
191            ViewMode::Report => {
192                self.render_generated_report(chunks[1], buf, state);
193            }
194        }
195
196        // Render status bar
197        self.render_status_bar(chunks[2], buf, state);
198    }
199}
200
201impl ReportsWidget {
202    fn render_no_data(&self, area: Rect, buf: &mut Buffer) {
203        let content = Paragraph::new(vec![
204            Line::from(""),
205            Line::from("No analysis data available"),
206            Line::from(""),
207            Line::from("Please run an analysis first before generating reports."),
208            Line::from(""),
209            Line::from("Press 'A' to go to the Analysis screen."),
210        ])
211        .alignment(Alignment::Center)
212        .block(
213            Block::default()
214                .borders(Borders::ALL)
215                .title("No Data")
216                .title_style(THEME.warning_style()),
217        );
218
219        content.render_ref(area, buf);
220    }
221
222    fn render_report_content(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
223        let chunks = Layout::default()
224            .direction(Direction::Horizontal)
225            .constraints([
226                Constraint::Percentage(40), // Format selection
227                Constraint::Percentage(60), // Preview/Export
228            ])
229            .split(area);
230
231        // Format selection
232        self.render_format_selection(chunks[0], buf, state);
233
234        // Preview/Export area
235        self.render_export_area(chunks[1], buf, state);
236    }
237
238    fn render_format_selection(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
239        let formats = [
240            (
241                "Summary",
242                ReportFormat::Summary,
243                "Quick overview with key findings",
244            ),
245            (
246                "Detailed",
247                ReportFormat::Detailed,
248                "Complete issue breakdown",
249            ),
250            ("JSON", ReportFormat::Json, "Machine-readable format"),
251            (
252                "Markdown",
253                ReportFormat::Markdown,
254                "Documentation-friendly format",
255            ),
256        ];
257
258        let items: Vec<ListItem> = formats
259            .iter()
260            .map(|(name, format, description)| {
261                let is_selected = *format == state.selected_format;
262                let style = if is_selected {
263                    THEME.selected_style()
264                } else {
265                    Style::default()
266                };
267
268                ListItem::new(vec![
269                    Line::from(vec![Span::styled(
270                        *name,
271                        if is_selected {
272                            THEME.selected_style()
273                        } else {
274                            THEME.text_primary.into()
275                        },
276                    )]),
277                    Line::from(vec![Span::styled(*description, THEME.info_style())]),
278                ])
279                .style(style)
280            })
281            .collect();
282
283        let format_list = List::new(items).block(
284            Block::default()
285                .borders(Borders::ALL)
286                .title("Export Format")
287                .title_style(THEME.header_style()),
288        );
289
290        WidgetRef::render_ref(&format_list, area, buf);
291    }
292
293    fn render_export_area(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
294        if let Some(review) = &state.review {
295            let chunks = Layout::default()
296                .direction(Direction::Vertical)
297                .constraints([
298                    Constraint::Length(8), // Preview
299                    Constraint::Length(5), // Export button
300                    Constraint::Min(3),    // Export status
301                ])
302                .split(area);
303
304            // Preview
305            self.render_preview(chunks[0], buf, state, review);
306
307            // Export button
308            self.render_export_button(chunks[1], buf);
309
310            // Export status
311            self.render_export_status(chunks[2], buf, state);
312        }
313    }
314
315    fn render_preview(
316        &self,
317        area: Rect,
318        buf: &mut Buffer,
319        state: &ReportsWidgetState,
320        review: &crate::core::review::Review,
321    ) {
322        let preview_content = match state.selected_format {
323            ReportFormat::Summary => {
324                vec![
325                    Line::from("# Code Review Summary"),
326                    Line::from(""),
327                    Line::from(format!("Files analyzed: {}", review.files_count)),
328                    Line::from(format!("Total issues: {}", review.issues_count)),
329                    Line::from(format!("Critical: {}", review.critical_issues)),
330                    Line::from(format!("High: {}", review.high_issues)),
331                ]
332            }
333            ReportFormat::Detailed => {
334                vec![
335                    Line::from("# Detailed Code Review Report"),
336                    Line::from(""),
337                    Line::from("## Issues Found:"),
338                    Line::from(format!("- {} Critical issues", review.critical_issues)),
339                    Line::from(format!("- {} High priority issues", review.high_issues)),
340                    Line::from("(Full details in exported file)"),
341                ]
342            }
343            ReportFormat::Json => {
344                vec![
345                    Line::from("{"),
346                    Line::from(
347                        "  \"files_count\": {},".replace("{}", &review.files_count.to_string()),
348                    ),
349                    Line::from(
350                        "  \"issues_count\": {},".replace("{}", &review.issues_count.to_string()),
351                    ),
352                    Line::from(
353                        "  \"critical_issues\": {},"
354                            .replace("{}", &review.critical_issues.to_string()),
355                    ),
356                    Line::from("  \"issues\": [...]"),
357                    Line::from("}"),
358                ]
359            }
360            ReportFormat::Markdown => {
361                vec![
362                    Line::from("# Code Review Report"),
363                    Line::from(""),
364                    Line::from("## Summary"),
365                    Line::from(format!("- **Files analyzed**: {}", review.files_count)),
366                    Line::from(format!("- **Total issues**: {}", review.issues_count)),
367                    Line::from(""),
368                    Line::from("## Issues"),
369                ]
370            }
371        };
372
373        let preview = Paragraph::new(preview_content)
374            .block(
375                Block::default()
376                    .borders(Borders::ALL)
377                    .title("Preview")
378                    .title_style(THEME.header_style()),
379            )
380            .wrap(ratatui::widgets::Wrap { trim: true });
381
382        preview.render_ref(area, buf);
383    }
384
385    fn render_export_button(&self, area: Rect, buf: &mut Buffer) {
386        let button = Paragraph::new("� Generate Report (Press Enter)")
387            .style(THEME.button_style(false))
388            .alignment(Alignment::Center)
389            .block(
390                Block::default()
391                    .borders(Borders::ALL)
392                    .border_style(Style::default().fg(THEME.primary)),
393            );
394
395        button.render_ref(area, buf);
396    }
397
398    fn render_export_status(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
399        let (status_text, status_style) = match &state.export_status {
400            ExportStatus::None => ("Ready to export".to_string(), THEME.info_style()),
401            ExportStatus::Exporting(format) => (
402                format!("Exporting {format} report..."),
403                THEME.warning_style(),
404            ),
405            ExportStatus::Success(path) => (
406                format!("✅ Exported successfully to: {path}"),
407                THEME.success_style(),
408            ),
409        };
410
411        let status = Paragraph::new(status_text)
412            .style(status_style)
413            .alignment(Alignment::Center)
414            .block(
415                Block::default()
416                    .borders(Borders::ALL)
417                    .title("Status")
418                    .title_style(THEME.header_style()),
419            );
420
421        status.render_ref(area, buf);
422    }
423
424    fn render_status_bar(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
425        let status_text = match state.view_mode {
426            ViewMode::Selection => {
427                if state.review.is_some() {
428                    "Use ←→ or Tab to change format, Enter to generate report, A for analysis, Esc to go back"
429                } else {
430                    "A to run analysis, Esc to go back"
431                }
432            }
433            ViewMode::Report => "Enter to export report, Esc to go back to selection",
434        };
435
436        let status = Paragraph::new(status_text)
437            .style(THEME.info_style())
438            .alignment(Alignment::Center)
439            .block(
440                Block::default()
441                    .borders(Borders::TOP)
442                    .border_style(THEME.info_style()),
443            );
444
445        status.render_ref(area, buf);
446    }
447
448    fn render_generated_report(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
449        if let Some(report_content) = &state.generated_report {
450            // Split the report into lines for scrollable display
451            let lines: Vec<Line> = report_content
452                .lines()
453                .map(|line| Line::from(line.to_string()))
454                .collect();
455
456            let report = Paragraph::new(lines)
457                .block(
458                    Block::default()
459                        .borders(Borders::ALL)
460                        .title(format!(
461                            " {} Report ",
462                            match state.selected_format {
463                                ReportFormat::Summary => "Summary",
464                                ReportFormat::Detailed => "Detailed",
465                                ReportFormat::Json => "JSON",
466                                ReportFormat::Markdown => "Markdown",
467                            }
468                        ))
469                        .title_style(THEME.header_style()),
470                )
471                .wrap(ratatui::widgets::Wrap { trim: false })
472                .scroll((0, 0)); // TODO: Add scrolling support
473
474            report.render_ref(area, buf);
475        } else {
476            let error = Paragraph::new("No report generated")
477                .alignment(Alignment::Center)
478                .block(
479                    Block::default()
480                        .borders(Borders::ALL)
481                        .title("Error")
482                        .title_style(THEME.error_style()),
483                );
484            error.render_ref(area, buf);
485        }
486    }
487}