use cai_core::Entry;
use cai_storage::Storage;
use ratatui::style::Color;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Normal,
Query,
Search,
Detail,
Help,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppState {
Running,
Quitting,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortOrder {
Asc,
Desc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Column {
Timestamp,
Source,
Prompt,
}
pub struct App<S>
where
S: Storage + ?Sized,
{
storage: Arc<S>,
pub mode: Mode,
pub state: AppState,
pub query_input: String,
pub search_input: String,
pub entries: Vec<Entry>,
pub selected: usize,
pub scroll: usize,
pub sort_column: Column,
pub sort_order: SortOrder,
pub status_message: String,
pub status_color: Color,
pub status_timestamp: u64,
pub history: Vec<String>,
pub history_index: Option<usize>,
pub detail_scroll: usize,
pub help_scroll: usize,
}
impl<S> App<S>
where
S: Storage,
{
pub fn new(storage: Arc<S>) -> Self {
Self {
storage,
mode: Mode::Normal,
state: AppState::Running,
query_input: String::new(),
search_input: String::new(),
entries: Vec::new(),
selected: 0,
scroll: 0,
sort_column: Column::Timestamp,
sort_order: SortOrder::Desc,
status_message: "Press '/' to search, 'i' to enter query mode, 'q' to quit".to_string(),
status_color: Color::Gray,
status_timestamp: now(),
history: Vec::new(),
history_index: None,
detail_scroll: 0,
help_scroll: 0,
}
}
pub async fn execute_query(&mut self, query: &str) {
if !query.is_empty() {
self.history.push(query.to_string());
self.history_index = None;
}
match self.storage.query(None).await {
Ok(entries) => {
self.entries = entries;
self.selected = 0;
self.scroll = 0;
self.sort_entries();
self.set_status(
format!("Query returned {} results", self.entries.len()),
Color::Green,
);
}
Err(e) => {
self.set_status(format!("Query error: {}", e), Color::Red);
}
}
}
pub fn search(&mut self) {
if self.search_input.is_empty() {
return;
}
let query = self.search_input.to_lowercase();
self.entries.retain(|entry| {
entry.prompt.to_lowercase().contains(&query)
|| entry.response.to_lowercase().contains(&query)
|| format!("{:?}", entry.source)
.to_lowercase()
.contains(&query)
});
self.selected = 0;
self.scroll = 0;
self.set_status(
format!("Found {} results", self.entries.len()),
Color::Green,
);
}
pub async fn clear_search(&mut self) {
self.search_input.clear();
self.execute_query("").await;
}
pub fn set_status(&mut self, msg: String, color: Color) {
self.status_message = msg;
self.status_color = color;
self.status_timestamp = now();
}
pub fn should_clear_status(&self) -> bool {
now() - self.status_timestamp > 5
}
pub fn reset_status(&mut self) {
self.status_message =
"Press '/' to search, 'i' to enter query mode, 'q' to quit".to_string();
self.status_color = Color::Gray;
}
pub fn select_previous(&mut self) {
if !self.entries.is_empty() && self.selected > 0 {
self.selected -= 1;
if self.selected < self.scroll {
self.scroll = self.selected;
}
}
}
pub fn select_next(&mut self, height: usize) {
if !self.entries.is_empty() {
self.selected = self.selected.saturating_add(1).min(self.entries.len() - 1);
let visible_height = height.saturating_sub(4);
if self.selected >= self.scroll + visible_height && visible_height > 0 {
self.scroll = self.selected - visible_height + 1;
}
}
}
pub fn sort_entries(&mut self) {
match self.sort_column {
Column::Timestamp => {
self.entries.sort_by(|a, b| {
if self.sort_order == SortOrder::Asc {
a.timestamp.cmp(&b.timestamp)
} else {
b.timestamp.cmp(&a.timestamp)
}
});
}
Column::Source => {
self.entries.sort_by(|a, b| {
let source_cmp = format!("{:?}", a.source).cmp(&format!("{:?}", b.source));
if self.sort_order == SortOrder::Asc {
source_cmp
} else {
source_cmp.reverse()
}
});
}
Column::Prompt => {
self.entries.sort_by(|a, b| {
let prompt_cmp = a.prompt.cmp(&b.prompt);
if self.sort_order == SortOrder::Asc {
prompt_cmp
} else {
prompt_cmp.reverse()
}
});
}
}
}
pub fn toggle_sort(&mut self, column: Column) {
if self.sort_column == column {
self.sort_order = match self.sort_order {
SortOrder::Asc => SortOrder::Desc,
SortOrder::Desc => SortOrder::Asc,
};
} else {
self.sort_column = column;
self.sort_order = SortOrder::Asc;
}
self.sort_entries();
}
pub fn history_previous(&mut self) {
if self.history.is_empty() {
return;
}
match self.history_index {
None => {
self.history_index = Some(self.history.len() - 1);
}
Some(idx) if idx > 0 => {
self.history_index = Some(idx - 1);
}
_ => {}
}
if let Some(idx) = self.history_index {
self.query_input = self.history[idx].clone();
}
}
pub fn history_next(&mut self) {
if self.history.is_empty() {
return;
}
match self.history_index {
Some(idx) if idx < self.history.len() - 1 => {
self.history_index = Some(idx + 1);
if let Some(idx) = self.history_index {
self.query_input = self.history[idx].clone();
}
}
Some(_) => {
self.history_index = None;
self.query_input.clear();
}
None => {}
}
}
pub fn selected_entry(&self) -> Option<&Entry> {
self.entries.get(self.selected)
}
pub fn row_style(&self, index: usize) -> ratatui::style::Style {
use ratatui::style::Style;
if index == self.selected {
Style::default().bg(ratatui::style::Color::DarkGray)
} else {
Style::default()
}
}
pub fn detail_scroll_down(&mut self) {
self.detail_scroll = self.detail_scroll.saturating_add(1);
}
pub fn detail_scroll_up(&mut self) {
self.detail_scroll = self.detail_scroll.saturating_sub(1);
}
pub fn detail_scroll_reset(&mut self) {
self.detail_scroll = 0;
}
pub fn help_scroll_down(&mut self) {
self.help_scroll = self.help_scroll.saturating_add(1);
}
pub fn help_scroll_up(&mut self) {
self.help_scroll = self.help_scroll.saturating_sub(1);
}
}
fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}