Skip to main content

graphrag_cli/ui/components/
raw_results_viewer.rs

1//! Raw results viewer component - shows search results before LLM processing
2
3use crate::{action::Action, theme::Theme};
4use ratatui::{
5    layout::{Margin, Rect},
6    widgets::{Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap},
7    Frame,
8};
9
10/// Raw results viewer with scrolling support
11pub struct RawResultsViewer {
12    /// Display content (lines)
13    content: Vec<String>,
14    /// Vertical scroll position
15    scroll_offset: usize,
16    /// Scrollbar state
17    scrollbar_state: ScrollbarState,
18    /// Is this widget focused?
19    focused: bool,
20    /// Theme
21    theme: Theme,
22}
23
24impl RawResultsViewer {
25    pub fn new() -> Self {
26        Self {
27            content: vec![
28                "Raw search results will appear here.".to_string(),
29                "".to_string(),
30                "These are the entities and relationships retrieved from".to_string(),
31                "the knowledge graph before LLM processing.".to_string(),
32            ],
33            scroll_offset: 0,
34            scrollbar_state: ScrollbarState::default(),
35            focused: false,
36            theme: Theme::default(),
37        }
38    }
39
40    /// Set content from search results
41    pub fn set_content(&mut self, lines: Vec<String>) {
42        self.content = lines;
43        self.scroll_offset = 0;
44        self.update_scrollbar();
45    }
46
47    /// Clear content
48    #[allow(dead_code)]
49    pub fn clear(&mut self) {
50        self.content.clear();
51        self.scroll_offset = 0;
52        self.update_scrollbar();
53    }
54
55    /// Scroll up one line
56    pub fn scroll_up(&mut self) {
57        self.scroll_offset = self.scroll_offset.saturating_sub(1);
58        self.update_scrollbar();
59    }
60
61    /// Scroll down one line
62    pub fn scroll_down(&mut self) {
63        if self.scroll_offset < self.content.len().saturating_sub(1) {
64            self.scroll_offset += 1;
65        }
66        self.update_scrollbar();
67    }
68
69    /// Scroll up one page
70    pub fn scroll_page_up(&mut self, page_size: usize) {
71        self.scroll_offset = self.scroll_offset.saturating_sub(page_size);
72        self.update_scrollbar();
73    }
74
75    /// Scroll down one page
76    pub fn scroll_page_down(&mut self, page_size: usize) {
77        let max_scroll = self.content.len().saturating_sub(1);
78        self.scroll_offset = (self.scroll_offset + page_size).min(max_scroll);
79        self.update_scrollbar();
80    }
81
82    /// Scroll to top
83    pub fn scroll_to_top(&mut self) {
84        self.scroll_offset = 0;
85        self.update_scrollbar();
86    }
87
88    /// Scroll to bottom
89    pub fn scroll_to_bottom(&mut self) {
90        self.scroll_offset = self.content.len().saturating_sub(1);
91        self.update_scrollbar();
92    }
93
94    /// Set focused state
95    pub fn set_focused(&mut self, focused: bool) {
96        self.focused = focused;
97    }
98
99    /// Update scrollbar state
100    fn update_scrollbar(&mut self) {
101        self.scrollbar_state = self
102            .scrollbar_state
103            .content_length(self.content.len())
104            .position(self.scroll_offset);
105    }
106}
107
108impl super::Component for RawResultsViewer {
109    fn handle_action(&mut self, action: &Action) -> Option<Action> {
110        match action {
111            Action::ScrollUp => {
112                if self.focused {
113                    self.scroll_up();
114                }
115                None
116            },
117            Action::ScrollDown => {
118                if self.focused {
119                    self.scroll_down();
120                }
121                None
122            },
123            Action::ScrollPageUp => {
124                if self.focused {
125                    self.scroll_page_up(10);
126                }
127                None
128            },
129            Action::ScrollPageDown => {
130                if self.focused {
131                    self.scroll_page_down(10);
132                }
133                None
134            },
135            Action::ScrollToTop => {
136                if self.focused {
137                    self.scroll_to_top();
138                }
139                None
140            },
141            Action::ScrollToBottom => {
142                if self.focused {
143                    self.scroll_to_bottom();
144                }
145                None
146            },
147            Action::FocusRawResultsViewer => {
148                self.set_focused(true);
149                None
150            },
151            _ => None,
152        }
153    }
154
155    fn render(&mut self, f: &mut Frame, area: Rect) {
156        let border_style = if self.focused {
157            self.theme.border_focused()
158        } else {
159            self.theme.border()
160        };
161
162        let title = if self.focused {
163            " Raw Search Results [ACTIVE] (j/k or ↑↓ to scroll | Ctrl+N next panel) "
164        } else {
165            " Raw Search Results (Ctrl+3 or Ctrl+N to focus) "
166        };
167
168        let block = Block::default()
169            .title(title)
170            .borders(Borders::ALL)
171            .border_style(border_style);
172
173        let text = self.content.join("\n");
174
175        let paragraph = Paragraph::new(text)
176            .block(block)
177            .wrap(Wrap { trim: false })
178            .scroll((self.scroll_offset as u16, 0))
179            .style(self.theme.text_dim()); // Use dimmed text for raw results
180
181        f.render_widget(paragraph, area);
182
183        // Render scrollbar if content is larger than area
184        if self.content.len() > area.height as usize {
185            let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
186                .begin_symbol(Some("↑"))
187                .end_symbol(Some("↓"));
188
189            let scrollbar_area = area.inner(Margin {
190                vertical: 1,
191                horizontal: 0,
192            });
193
194            f.render_stateful_widget(scrollbar, scrollbar_area, &mut self.scrollbar_state);
195        }
196    }
197}
198
199impl Default for RawResultsViewer {
200    fn default() -> Self {
201        Self::new()
202    }
203}