Skip to main content

graphrag_cli/ui/components/
query_input.rs

1//! Query input component
2//!
3//! Single input box that automatically detects slash commands
4
5use crate::{action::Action, theme::Theme};
6use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
7use ratatui::{
8    layout::Rect,
9    style::{Modifier, Style},
10    widgets::{Block, Borders},
11    Frame,
12};
13use tui_textarea::TextArea;
14
15/// Query input widget - handles both queries and slash commands
16pub struct QueryInput {
17    /// Text area for input
18    textarea: TextArea<'static>,
19    /// Is this widget focused?
20    focused: bool,
21    /// Theme
22    theme: Theme,
23}
24
25impl QueryInput {
26    pub fn new() -> Self {
27        let mut textarea = TextArea::default();
28        textarea.set_cursor_line_style(Style::default());
29        textarea.set_placeholder_text("Enter query or /command... (e.g., \"What are the main entities?\" or \"/config file.json5\")");
30
31        Self {
32            textarea,
33            focused: true,
34            theme: Theme::default(),
35        }
36    }
37
38    /// Handle keyboard input directly
39    /// Returns Some(Action) if an action should be triggered, None if key was consumed for input
40    pub fn handle_key(&mut self, key: KeyEvent) -> Option<Action> {
41        // Only handle keys when input is focused
42        if !self.focused {
43            return None;
44        }
45
46        // Handle special keys that should trigger actions
47        match (key.code, key.modifiers) {
48            // Submit on Enter
49            (KeyCode::Enter, KeyModifiers::NONE) => {
50                let content = self.textarea.lines().join("\n");
51
52                if content.trim().is_empty() {
53                    return Some(Action::SetStatus(
54                        crate::action::StatusType::Warning,
55                        "Cannot submit empty input".to_string(),
56                    ));
57                }
58
59                // Clear textarea
60                self.textarea = TextArea::default();
61                self.textarea.set_cursor_line_style(Style::default());
62                self.textarea.set_placeholder_text("Enter query or /command... (e.g., \"What are the main entities?\" or \"/config file.json5\")");
63
64                // Auto-detect: slash command vs query
65                if crate::mode::is_slash_command(&content) {
66                    Some(Action::ExecuteSlashCommand(content))
67                } else {
68                    Some(Action::ExecuteQuery(content))
69                }
70            },
71            // Clear input (consume the key, don't pass to scrolling)
72            (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
73                self.textarea = TextArea::default();
74                self.textarea.set_cursor_line_style(Style::default());
75                self.textarea.set_placeholder_text("Enter query or /command... (e.g., \"What are the main entities?\" or \"/config file.json5\")");
76                Some(Action::Noop) // Return Noop to indicate key was consumed
77            },
78            // Let textarea handle everything else - return Noop to indicate consumption
79            _ => {
80                self.textarea.input(key);
81                Some(Action::Noop) // Key was consumed by input
82            },
83        }
84    }
85
86    /// Set focused state
87    pub fn set_focused(&mut self, focused: bool) {
88        self.focused = focused;
89    }
90}
91
92impl super::Component for QueryInput {
93    fn handle_action(&mut self, action: &Action) -> Option<Action> {
94        match action {
95            Action::FocusQueryInput => {
96                self.set_focused(true);
97                None
98            },
99            _ => None,
100        }
101    }
102
103    fn render(&mut self, f: &mut Frame, area: Rect) {
104        let border_color = if self.focused {
105            ratatui::style::Color::Green
106        } else {
107            self.theme.border
108        };
109
110        let title = if self.focused {
111            "💬 Input (Enter to submit | Ctrl+D to clear | Ctrl+N to focus panels)"
112        } else {
113            "💬 Input (Ctrl+N/Ctrl+P to cycle panels | Esc to return here)"
114        };
115
116        let block = Block::default()
117            .title(title)
118            .borders(Borders::ALL)
119            .border_style(
120                Style::default()
121                    .fg(border_color)
122                    .add_modifier(if self.focused {
123                        Modifier::BOLD
124                    } else {
125                        Modifier::empty()
126                    }),
127            );
128
129        self.textarea.set_block(block);
130        self.textarea.set_cursor_style(if self.focused {
131            Style::default().add_modifier(Modifier::REVERSED)
132        } else {
133            Style::default()
134        });
135
136        f.render_widget(&self.textarea, area);
137    }
138}
139
140impl Default for QueryInput {
141    fn default() -> Self {
142        Self::new()
143    }
144}