graphrag_cli/ui/components/
query_input.rs1use 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
15pub struct QueryInput {
17 textarea: TextArea<'static>,
19 focused: bool,
21 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 pub fn handle_key(&mut self, key: KeyEvent) -> Option<Action> {
41 if !self.focused {
43 return None;
44 }
45
46 match (key.code, key.modifiers) {
48 (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 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 if crate::mode::is_slash_command(&content) {
66 Some(Action::ExecuteSlashCommand(content))
67 } else {
68 Some(Action::ExecuteQuery(content))
69 }
70 },
71 (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) },
78 _ => {
80 self.textarea.input(key);
81 Some(Action::Noop) },
83 }
84 }
85
86 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}