1use cai_core::Entry;
4use cai_storage::Storage;
5use ratatui::style::Color;
6use std::sync::Arc;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Mode {
12 Normal,
14 Query,
16 Search,
18 Detail,
20 Help,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum AppState {
27 Running,
29 Quitting,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum SortOrder {
36 Asc,
38 Desc,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Column {
45 Timestamp,
47 Source,
49 Prompt,
51}
52
53pub struct App<S>
55where
56 S: Storage + ?Sized,
57{
58 storage: Arc<S>,
60 pub mode: Mode,
62 pub state: AppState,
64 pub query_input: String,
66 pub search_input: String,
68 pub entries: Vec<Entry>,
70 pub selected: usize,
72 pub scroll: usize,
74 pub sort_column: Column,
76 pub sort_order: SortOrder,
78 pub status_message: String,
80 pub status_color: Color,
82 pub status_timestamp: u64,
84 pub history: Vec<String>,
86 pub history_index: Option<usize>,
88 pub detail_scroll: usize,
90 pub help_scroll: usize,
92}
93
94impl<S> App<S>
95where
96 S: Storage,
97{
98 pub fn new(storage: Arc<S>) -> Self {
100 Self {
101 storage,
102 mode: Mode::Normal,
103 state: AppState::Running,
104 query_input: String::new(),
105 search_input: String::new(),
106 entries: Vec::new(),
107 selected: 0,
108 scroll: 0,
109 sort_column: Column::Timestamp,
110 sort_order: SortOrder::Desc,
111 status_message: "Press '/' to search, 'i' to enter query mode, 'q' to quit".to_string(),
112 status_color: Color::Gray,
113 status_timestamp: now(),
114 history: Vec::new(),
115 history_index: None,
116 detail_scroll: 0,
117 help_scroll: 0,
118 }
119 }
120
121 pub async fn execute_query(&mut self, query: &str) {
123 if !query.is_empty() {
125 self.history.push(query.to_string());
126 self.history_index = None;
127 }
128
129 match self.storage.query(None).await {
132 Ok(entries) => {
133 self.entries = entries;
134 self.selected = 0;
135 self.scroll = 0;
136 self.sort_entries();
137 self.set_status(
138 format!("Query returned {} results", self.entries.len()),
139 Color::Green,
140 );
141 }
142 Err(e) => {
143 self.set_status(format!("Query error: {}", e), Color::Red);
144 }
145 }
146 }
147
148 pub fn search(&mut self) {
150 if self.search_input.is_empty() {
151 return;
152 }
153
154 let query = self.search_input.to_lowercase();
155 self.entries.retain(|entry| {
156 entry.prompt.to_lowercase().contains(&query)
157 || entry.response.to_lowercase().contains(&query)
158 || format!("{:?}", entry.source)
159 .to_lowercase()
160 .contains(&query)
161 });
162
163 self.selected = 0;
164 self.scroll = 0;
165 self.set_status(
166 format!("Found {} results", self.entries.len()),
167 Color::Green,
168 );
169 }
170
171 pub async fn clear_search(&mut self) {
173 self.search_input.clear();
174 self.execute_query("").await;
175 }
176
177 pub fn set_status(&mut self, msg: String, color: Color) {
179 self.status_message = msg;
180 self.status_color = color;
181 self.status_timestamp = now();
182 }
183
184 pub fn should_clear_status(&self) -> bool {
186 now() - self.status_timestamp > 5
187 }
188
189 pub fn reset_status(&mut self) {
191 self.status_message =
192 "Press '/' to search, 'i' to enter query mode, 'q' to quit".to_string();
193 self.status_color = Color::Gray;
194 }
195
196 pub fn select_previous(&mut self) {
198 if !self.entries.is_empty() && self.selected > 0 {
199 self.selected -= 1;
200 if self.selected < self.scroll {
201 self.scroll = self.selected;
202 }
203 }
204 }
205
206 pub fn select_next(&mut self, height: usize) {
208 if !self.entries.is_empty() {
209 self.selected = self.selected.saturating_add(1).min(self.entries.len() - 1);
210 let visible_height = height.saturating_sub(4);
212 if self.selected >= self.scroll + visible_height && visible_height > 0 {
213 self.scroll = self.selected - visible_height + 1;
214 }
215 }
216 }
217
218 pub fn sort_entries(&mut self) {
220 match self.sort_column {
221 Column::Timestamp => {
222 self.entries.sort_by(|a, b| {
223 if self.sort_order == SortOrder::Asc {
224 a.timestamp.cmp(&b.timestamp)
225 } else {
226 b.timestamp.cmp(&a.timestamp)
227 }
228 });
229 }
230 Column::Source => {
231 self.entries.sort_by(|a, b| {
232 let source_cmp = format!("{:?}", a.source).cmp(&format!("{:?}", b.source));
233 if self.sort_order == SortOrder::Asc {
234 source_cmp
235 } else {
236 source_cmp.reverse()
237 }
238 });
239 }
240 Column::Prompt => {
241 self.entries.sort_by(|a, b| {
242 let prompt_cmp = a.prompt.cmp(&b.prompt);
243 if self.sort_order == SortOrder::Asc {
244 prompt_cmp
245 } else {
246 prompt_cmp.reverse()
247 }
248 });
249 }
250 }
251 }
252
253 pub fn toggle_sort(&mut self, column: Column) {
255 if self.sort_column == column {
256 self.sort_order = match self.sort_order {
257 SortOrder::Asc => SortOrder::Desc,
258 SortOrder::Desc => SortOrder::Asc,
259 };
260 } else {
261 self.sort_column = column;
262 self.sort_order = SortOrder::Asc;
263 }
264 self.sort_entries();
265 }
266
267 pub fn history_previous(&mut self) {
269 if self.history.is_empty() {
270 return;
271 }
272
273 match self.history_index {
274 None => {
275 self.history_index = Some(self.history.len() - 1);
276 }
277 Some(idx) if idx > 0 => {
278 self.history_index = Some(idx - 1);
279 }
280 _ => {}
281 }
282
283 if let Some(idx) = self.history_index {
284 self.query_input = self.history[idx].clone();
285 }
286 }
287
288 pub fn history_next(&mut self) {
290 if self.history.is_empty() {
291 return;
292 }
293
294 match self.history_index {
295 Some(idx) if idx < self.history.len() - 1 => {
296 self.history_index = Some(idx + 1);
297 if let Some(idx) = self.history_index {
298 self.query_input = self.history[idx].clone();
299 }
300 }
301 Some(_) => {
302 self.history_index = None;
303 self.query_input.clear();
304 }
305 None => {}
306 }
307 }
308
309 pub fn selected_entry(&self) -> Option<&Entry> {
311 self.entries.get(self.selected)
312 }
313
314 pub fn row_style(&self, index: usize) -> ratatui::style::Style {
316 use ratatui::style::Style;
317 if index == self.selected {
318 Style::default().bg(ratatui::style::Color::DarkGray)
319 } else {
320 Style::default()
321 }
322 }
323
324 pub fn detail_scroll_down(&mut self) {
326 self.detail_scroll = self.detail_scroll.saturating_add(1);
327 }
328
329 pub fn detail_scroll_up(&mut self) {
331 self.detail_scroll = self.detail_scroll.saturating_sub(1);
332 }
333
334 pub fn detail_scroll_reset(&mut self) {
336 self.detail_scroll = 0;
337 }
338
339 pub fn help_scroll_down(&mut self) {
341 self.help_scroll = self.help_scroll.saturating_add(1);
342 }
343
344 pub fn help_scroll_up(&mut self) {
346 self.help_scroll = self.help_scroll.saturating_sub(1);
347 }
348}
349
350fn now() -> u64 {
352 SystemTime::now()
353 .duration_since(UNIX_EPOCH)
354 .unwrap_or_default()
355 .as_secs()
356}