semantic_diff/ui/
review_view.rs1use crate::app::App;
2use crate::preview::markdown::{parse_markdown, PreviewBlock};
3use crate::review::{ReviewSection, ReviewSource, SectionState};
4use ratatui::layout::Rect;
5use ratatui::style::{Color, Modifier, Style};
6use ratatui::text::{Line, Span};
7use ratatui::widgets::{Paragraph, Wrap};
8use ratatui::Frame;
9
10pub fn render_review_with_diff(app: &App, frame: &mut Frame, area: Rect) {
12 let mut lines: Vec<Line> = Vec::new();
13
14 render_banner(&app.review_source, &mut lines, &app.theme);
16
17 if let Some(hash) = app.active_review_group {
19 if let Some(review) = app.review_cache.get(&hash) {
20 let label = find_group_label(app, hash);
22 let completed = review
23 .sections
24 .values()
25 .filter(|s| s.is_complete())
26 .count();
27 lines.push(Line::from(vec![
28 Span::styled(
29 format!(" Review: \"{}\"", label),
30 Style::default()
31 .fg(app.theme.help_section_fg)
32 .add_modifier(Modifier::BOLD),
33 ),
34 Span::styled(
35 format!(" [{}/4]", completed),
36 Style::default().fg(app.theme.help_dismiss_fg),
37 ),
38 ]));
39 lines.push(Line::raw(""));
40
41 for section in ReviewSection::all() {
43 if let Some(state) = review.sections.get(§ion) {
44 render_section(section, state, &mut lines, &app.theme, area.width);
45 }
46 }
47 }
48 }
49
50 let hr = "─".repeat(area.width as usize);
52 lines.push(Line::raw(""));
53 lines.push(Line::from(Span::styled(
54 hr,
55 Style::default().fg(app.theme.help_dismiss_fg),
56 )));
57 lines.push(Line::raw(""));
58
59 let diff_lines = build_diff_lines(app);
61 lines.extend(diff_lines);
62
63 let scroll = app.review_scroll as u16;
65 let paragraph = Paragraph::new(lines)
66 .scroll((scroll, 0))
67 .wrap(Wrap { trim: false });
68 frame.render_widget(paragraph, area);
69}
70
71fn render_banner(source: &ReviewSource, lines: &mut Vec<Line>, theme: &crate::theme::Theme) {
72 match source {
73 ReviewSource::Skill { name, path } => {
74 lines.push(Line::from(vec![
75 Span::styled(
76 " Reviewed with: ",
77 Style::default().fg(theme.help_dismiss_fg),
78 ),
79 Span::styled(
80 format!("\"{}\"", name),
81 Style::default()
82 .fg(theme.help_section_fg)
83 .add_modifier(Modifier::BOLD),
84 ),
85 ]));
86 lines.push(Line::from(Span::styled(
87 format!(" {}", path.display()),
88 Style::default().fg(theme.help_dismiss_fg),
89 )));
90 }
91 ReviewSource::BuiltIn => {
92 lines.push(Line::from(Span::styled(
93 " Reviewed with: built-in generic reviewer",
94 Style::default().fg(theme.help_dismiss_fg),
95 )));
96 lines.push(Line::from(Span::styled(
97 " Tip: Create a review SKILL in .claude/skills/ or ~/.claude/skills/",
98 Style::default().fg(theme.help_dismiss_fg),
99 )));
100 }
101 }
102 lines.push(Line::raw(""));
103}
104
105fn render_section(
106 section: ReviewSection,
107 state: &SectionState,
108 lines: &mut Vec<Line>,
109 theme: &crate::theme::Theme,
110 width: u16,
111) {
112 match state {
113 SectionState::Loading => {
114 lines.push(Line::from(Span::styled(
115 format!(" {} Loading {}...", "⠋", section.label()),
116 Style::default().fg(Color::Yellow),
117 )));
118 lines.push(Line::raw(""));
119 }
120 SectionState::Ready(content) => {
121 lines.push(Line::from(Span::styled(
123 format!(" {} ", section.label()),
124 Style::default()
125 .fg(theme.help_section_fg)
126 .add_modifier(Modifier::BOLD),
127 )));
128
129 let blocks = parse_markdown(content, width.saturating_sub(2), theme);
131 for block in blocks {
132 match block {
133 PreviewBlock::Text(md_lines) => {
134 for line in md_lines {
135 let mut spans = vec![Span::raw(" ")];
137 spans.extend(line.spans);
138 lines.push(Line::from(spans));
139 }
140 }
141 PreviewBlock::Mermaid(mermaid_block) => {
142 lines.push(Line::from(Span::styled(
144 " ```mermaid".to_string(),
145 Style::default().fg(theme.help_dismiss_fg),
146 )));
147 for src_line in mermaid_block.source.lines() {
148 lines.push(Line::from(Span::styled(
149 format!(" {src_line}"),
150 Style::default().fg(theme.md_code_block_fg),
151 )));
152 }
153 lines.push(Line::from(Span::styled(
154 " ```".to_string(),
155 Style::default().fg(theme.help_dismiss_fg),
156 )));
157 }
158 }
159 }
160 lines.push(Line::raw(""));
161 }
162 SectionState::Error(msg) => {
163 lines.push(Line::from(Span::styled(
164 format!(" [{} failed: {}]", section.label(), msg),
165 Style::default()
166 .fg(Color::Red)
167 .add_modifier(Modifier::DIM),
168 )));
169 lines.push(Line::raw(""));
170 }
171 SectionState::Skipped => {
172 }
174 }
175}
176
177fn find_group_label(app: &App, hash: u64) -> String {
179 if let Some(groups) = &app.semantic_groups {
180 for group in groups {
181 if crate::review::group_content_hash(group) == hash {
182 return group.label.clone();
183 }
184 }
185 }
186 "Unknown".to_string()
187}
188
189fn build_diff_lines(app: &App) -> Vec<Line<'static>> {
191 let items = app.visible_items();
192 let mut lines = Vec::new();
193 for (idx, item) in items.iter().enumerate() {
194 let is_selected = idx == app.ui_state.selected_index;
195 lines.push(crate::ui::diff_view::render_item(app, item, is_selected));
196 }
197 lines
198}