diff --git a/lumni/src/apps/builtin/llm/prompt/src/app.rs b/lumni/src/apps/builtin/llm/prompt/src/app.rs index ec77784..32a0ece 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/app.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/app.rs @@ -20,9 +20,7 @@ use tokio::signal; use tokio::sync::Mutex; use tokio::time::{timeout, Duration}; -use super::chat::db::{ - ConversationDatabase, EncryptionHandler, ModelServerName, -}; +use super::chat::db::{ConversationDatabase, ModelServerName}; use super::chat::{ prompt_app, App, AssistantManager, ChatEvent, NewConversation, PromptInstruction, ThreadedChatSession, diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/mod.rs index 2bef906..65d7876 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/mod.rs @@ -13,7 +13,7 @@ use super::connector::{DatabaseConnector, DatabaseOperationError}; use super::encryption::EncryptionHandler; use crate::external as lumni; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct UserProfileDbHandler { profile_name: Option, db: Arc>, diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs index 907e83c..bfcd5e5 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs @@ -16,8 +16,9 @@ pub use super::error::{PromptError, PromptNotReadyReason}; use super::server::{CompletionResponse, ModelServer, ServerManager}; use super::tui::{ draw_ui, AppUi, ColorScheme, ColorSchemeType, CommandLineAction, - ConversationEvent, KeyEventHandler, ModalWindowType, PromptAction, - TextLine, TextSegment, TextWindowTrait, WindowEvent, WindowKind, + ConversationEvent, KeyEventHandler, ModalAction, ModalWindowType, + PromptAction, TextLine, TextSegment, TextWindowTrait, UserEvent, + WindowEvent, WindowKind, }; // gets PERSONAS from the generated code diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/session/conversation_loop.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/session/conversation_loop.rs index 0118840..3eecd35 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/session/conversation_loop.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/session/conversation_loop.rs @@ -14,9 +14,9 @@ use tokio::time::{interval, Duration}; use super::chat_session_manager::ChatEvent; use super::db::{ConversationDatabase, ConversationDbHandler}; use super::{ - App, ColorScheme, CommandLineAction, ConversationEvent, KeyEventHandler, - ModalWindowType, PromptAction, PromptInstruction, TextWindowTrait, - WindowEvent, WindowKind, + App, CommandLineAction, ConversationEvent, KeyEventHandler, ModalAction, + PromptAction, PromptInstruction, TextWindowTrait, UserEvent, WindowEvent, + WindowKind, }; pub use crate::external as lumni; @@ -36,15 +36,13 @@ pub async fn prompt_app( loop { tokio::select! { _ = tick.tick().fuse() => { - handle_tick( - terminal, + handle_input_event( &mut app, &mut redraw_ui, &mut current_mode, &mut key_event_handler, &keep_running, &mut db_conn, - &color_scheme ).await?; } _ = async { @@ -72,25 +70,34 @@ pub async fn prompt_app( if let Some(WindowEvent::Quit) = current_mode { break; } + + // Handle processing state + if app.is_processing { + redraw_ui = true; + + // Handle modal refresh if it's responsible for the processing state + if let Some(WindowEvent::Modal(_)) = current_mode { + current_mode = + Some(handle_modal_refresh(&mut app, &mut db_conn).await?); + } + } + + if redraw_ui { + app.draw_ui(terminal).await?; + redraw_ui = false; + } } Ok(()) } -async fn handle_tick( - terminal: &mut Terminal, +async fn handle_input_event( app: &mut App<'_>, redraw_ui: &mut bool, current_mode: &mut Option, key_event_handler: &mut KeyEventHandler, keep_running: &Arc, db_conn: &mut Arc, - color_scheme: &ColorScheme, ) -> Result<(), ApplicationError> { - if *redraw_ui { - app.draw_ui(terminal).await?; - *redraw_ui = false; - } - if poll(Duration::from_millis(1))? { let event = read()?; match event { @@ -102,7 +109,6 @@ async fn handle_tick( key_event, keep_running, db_conn, - color_scheme, ) .await?; } @@ -125,13 +131,12 @@ async fn handle_key_event( key_event: KeyEvent, keep_running: &Arc, db_conn: &mut Arc, - color_scheme: &ColorScheme, ) -> Result<(), ApplicationError> { - *current_mode = if let Some(mode) = current_mode.take() { + if let Some(mode) = current_mode.take() { let mut conversation_handler = db_conn.get_conversation_handler( app.get_conversation_id_for_active_session(), ); - key_event_handler + let new_window_event = key_event_handler .process_key( key_event, &mut app.ui, @@ -140,23 +145,75 @@ async fn handle_key_event( keep_running.clone(), &mut conversation_handler, ) - .await? - } else { - None - }; - match current_mode.as_mut() { - Some(WindowEvent::Prompt(prompt_action)) => { - handle_prompt_action(app, prompt_action.clone(), color_scheme) - .await?; - *current_mode = Some(app.ui.set_prompt_window(false)); + .await?; + let result = + handle_window_event(app, new_window_event, db_conn).await?; + *current_mode = Some(result); + } + + Ok(()) +} + +async fn handle_modal_refresh( + app: &mut App<'_>, + db_conn: &mut Arc, +) -> Result { + let modal = app + .ui + .modal + .as_mut() + .expect("Modal should exist when in Modal mode"); + let refresh_result = modal.refresh().await?; + match refresh_result { + WindowEvent::Modal(ModalAction::Refresh) => { + // If the modal still needs refreshing, keep the processing state + app.is_processing = true; + Ok(WindowEvent::Modal(ModalAction::Refresh)) } - Some(WindowEvent::CommandLine(action)) => { - handle_command_line_action(app, action.clone()); + other_event => { + // Handle the event returned by refresh + app.is_processing = false; + handle_window_event(app, other_event, db_conn).await } - Some(WindowEvent::Modal(modal_window_type)) => { - handle_modal_window(app, modal_window_type, db_conn).await?; + } +} + +async fn handle_window_event( + app: &mut App<'_>, + window_event: WindowEvent, + db_conn: &mut Arc, +) -> Result { + match window_event { + WindowEvent::Prompt(ref prompt_action) => { + handle_prompt_action(app, prompt_action.clone()).await?; + Ok(app.ui.set_prompt_window(false)) } - Some(WindowEvent::PromptWindow(event)) => { + WindowEvent::CommandLine(ref action) => { + handle_command_line_action(app, action.clone()); + Ok(window_event) + } + WindowEvent::Modal(ref action) => match action { + ModalAction::Refresh => { + app.is_processing = true; + Ok(window_event) + } + ModalAction::Open(ref modal_window_type) => { + app.ui + .set_new_modal( + modal_window_type.clone(), + db_conn, + app.get_conversation_id_for_active_session(), + ) + .await?; + Ok(window_event) + } + ModalAction::Event(ref user_event) => { + handle_modal_user_event(app, user_event, db_conn).await?; + Ok(window_event) + } + _ => Ok(window_event), + }, + WindowEvent::PromptWindow(ref event) => { let mut conversation_handler = db_conn.get_conversation_handler( app.get_conversation_id_for_active_session(), ); @@ -166,10 +223,10 @@ async fn handle_key_event( &mut conversation_handler, ) .await?; + Ok(window_event) } - _ => {} + _ => Ok(window_event), } - Ok(()) } fn handle_mouse_event(app: &mut App, mouse_event: MouseEvent) -> bool { @@ -190,11 +247,11 @@ fn handle_mouse_event(app: &mut App, mouse_event: MouseEvent) -> bool { async fn handle_prompt_action( app: &mut App<'_>, prompt_action: PromptAction, - color_scheme: &ColorScheme, ) -> Result<(), ApplicationError> { + let color_scheme = app.color_scheme.clone(); match prompt_action { PromptAction::Write(prompt) => { - send_prompt(app, &prompt, color_scheme).await?; + send_prompt(app, &prompt).await?; } PromptAction::Stop => { app.stop_active_chat_session().await?; @@ -226,27 +283,19 @@ fn handle_command_line_action( } } -async fn handle_modal_window( +async fn handle_modal_user_event( app: &mut App<'_>, - modal_window_type: &ModalWindowType, - db_conn: &mut Arc, + user_event: &UserEvent, + _db_conn: &mut Arc, ) -> Result<(), ApplicationError> { // switch to, or stay in modal window - match modal_window_type { - ModalWindowType::ConversationList(Some(_)) => { - // reload chat on any conversation event + match user_event { + UserEvent::ReloadConversation => { _ = app.reload_conversation().await; return Ok(()); } _ => {} } - - if app.ui.needs_modal_update(modal_window_type) { - let conversation_id = app.get_conversation_id_for_active_session(); - app.ui - .set_new_modal(modal_window_type.clone(), db_conn, conversation_id) - .await?; - } Ok(()) } @@ -283,9 +332,9 @@ async fn handle_prompt_window_event( async fn send_prompt<'a>( app: &mut App<'a>, prompt: &str, - color_scheme: &ColorScheme, ) -> Result<(), ApplicationError> { // prompt should end with single newline + let color_scheme = app.color_scheme.clone(); let formatted_prompt = format!("{}\n", prompt.trim_end()); let result = app .chat_manager diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/session/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/session/mod.rs index 405a52c..2f8a791 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/session/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/session/mod.rs @@ -16,10 +16,10 @@ pub use threaded_chat_session::ThreadedChatSession; use super::db::{ConversationDatabase, ConversationId}; use super::{ db, draw_ui, AppUi, ColorScheme, ColorSchemeType, CommandLineAction, - CompletionResponse, ConversationEvent, KeyEventHandler, ModalWindowType, - ModelServer, PromptAction, PromptError, PromptInstruction, - PromptNotReadyReason, ServerManager, TextWindowTrait, WindowEvent, - WindowKind, + CompletionResponse, ConversationEvent, KeyEventHandler, ModalAction, + ModalWindowType, ModelServer, PromptAction, PromptError, PromptInstruction, + PromptNotReadyReason, ServerManager, TextWindowTrait, UserEvent, + WindowEvent, WindowKind, }; pub use crate::external as lumni; @@ -27,6 +27,7 @@ pub struct App<'a> { pub ui: AppUi<'a>, pub chat_manager: ChatSessionManager, pub color_scheme: ColorScheme, + pub is_processing: bool, // flag to indicate if the app is busy processing } impl App<'_> { @@ -57,6 +58,7 @@ impl App<'_> { ui, chat_manager, color_scheme, + is_processing: false, }) } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_command_line.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_command_line.rs index 6200923..a76553b 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_command_line.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_command_line.rs @@ -7,7 +7,8 @@ use lumni::api::error::ApplicationError; use super::key_event::KeyTrack; use super::text_window_event::handle_text_window_event; use super::{ - AppUi, ModalWindowType, PromptAction, TextWindowTrait, WindowEvent, + AppUi, ModalAction, ModalWindowType, PromptAction, TextWindowTrait, + WindowEvent, }; pub use crate::external as lumni; @@ -15,7 +16,7 @@ pub fn handle_command_line_event( app_ui: &mut AppUi, key_track: &mut KeyTrack, is_running: Arc, -) -> Result, ApplicationError> { +) -> Result { let key_code = key_track.current_key().code; match key_code { @@ -25,7 +26,7 @@ pub fn handle_command_line_event( app_ui.command_line.text_empty(); app_ui.command_line.set_status_inactive(); - Ok(Some(app_ui.set_response_window())) + Ok(app_ui.set_response_window()) } KeyCode::Enter => { let command = app_ui.command_line.text_buffer().to_string(); @@ -33,29 +34,29 @@ pub fn handle_command_line_event( app_ui.command_line.set_status_inactive(); if command.starts_with(':') { match command.trim_start_matches(':') { - "q" => return Ok(Some(WindowEvent::Quit)), + "q" => return Ok(WindowEvent::Quit), "w" => { let question = app_ui.prompt.text_buffer().to_string(); app_ui.prompt.text_empty(); - return Ok(Some(WindowEvent::Prompt( - PromptAction::Write(question), + return Ok(WindowEvent::Prompt(PromptAction::Write( + question, ))); } "stop" => { - return Ok(Some(WindowEvent::Prompt( - PromptAction::Stop, - ))); + return Ok(WindowEvent::Prompt(PromptAction::Stop)); } _ => {} // command not recognized } } - Ok(Some(WindowEvent::PromptWindow(None))) + Ok(WindowEvent::PromptWindow(None)) } KeyCode::Char(':') => { // double-colon opens Modal (Config) window app_ui.command_line.text_empty(); app_ui.command_line.set_status_inactive(); - Ok(Some(WindowEvent::Modal(ModalWindowType::ProfileEdit))) + Ok(WindowEvent::Modal(ModalAction::Open( + ModalWindowType::ProfileEdit, + ))) } _ => handle_text_window_event( key_track, diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs index d3a6b42..aa8d946 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs @@ -16,34 +16,32 @@ pub fn handle_prompt_window_event( app_ui: &mut AppUi, key_track: &mut KeyTrack, is_running: Arc, -) -> Result, ApplicationError> { +) -> Result { match key_track.current_key().code { KeyCode::Up => { if !app_ui.response.text_buffer().is_empty() { let (_, row) = app_ui.prompt.get_column_row(); if row == 0 { // jump from prompt window to response window - return Ok(Some(app_ui.set_response_window())); + return Ok(app_ui.set_response_window()); } } } KeyCode::Tab => { if !in_editing_block(&mut app_ui.prompt) { - return Ok(Some(app_ui.prompt.next_window_status())); + return Ok(app_ui.prompt.next_window_status()); } } KeyCode::Enter => { // handle enter if not in editing mode if !app_ui.prompt.is_status_insert() { let question = app_ui.prompt.text_buffer().to_string(); - return Ok(Some(WindowEvent::Prompt(PromptAction::Write( - question, - )))); + return Ok(WindowEvent::Prompt(PromptAction::Write(question))); } } KeyCode::Backspace => { if app_ui.prompt.text_buffer().is_empty() { - return Ok(Some(app_ui.set_prompt_window(false))); + return Ok(app_ui.set_prompt_window(false)); } if !app_ui.prompt.is_status_insert() { // change to insert mode @@ -55,7 +53,7 @@ pub fn handle_prompt_window_event( if app_ui.prompt.is_status_insert() { ensure_closed_block(&mut app_ui.prompt)?; } - return Ok(Some(app_ui.set_prompt_window(false))); + return Ok(app_ui.set_prompt_window(false)); } KeyCode::Char(key) => { // catch Ctrl + shortcut key @@ -63,13 +61,13 @@ pub fn handle_prompt_window_event( match key { 'c' => { if app_ui.prompt.text_buffer().is_empty() { - return Ok(Some(WindowEvent::Quit)); + return Ok(WindowEvent::Quit); } else { app_ui.prompt.text_empty(); } } 'q' => { - return Ok(Some(WindowEvent::Quit)); + return Ok(WindowEvent::Quit); } 'a' => { app_ui.prompt.text_select_all(); @@ -79,15 +77,15 @@ pub fn handle_prompt_window_event( } _ => {} } - return Ok(Some(WindowEvent::PromptWindow(None))); + return Ok(WindowEvent::PromptWindow(None)); } else if !app_ui.prompt.is_status_insert() { // process regular key match key { 't' | 'T' => { - return Ok(Some(app_ui.set_response_window())); + return Ok(app_ui.set_response_window()); } 'i' | 'I' => { - return Ok(Some(app_ui.set_prompt_window(true))); + return Ok(app_ui.set_prompt_window(true)); } '+' => { app_ui.set_primary_window(WindowKind::PromptWindow); @@ -99,9 +97,7 @@ pub fn handle_prompt_window_event( if let Some(prev) = key_track.previous_key_str() { if prev == " " { // change to insert mode if double space - return Ok(Some( - app_ui.set_prompt_window(true), - )); + return Ok(app_ui.set_prompt_window(true)); } } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_response_window.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_response_window.rs index bf6dc9d..0aded46 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_response_window.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_response_window.rs @@ -13,42 +13,42 @@ pub fn handle_response_window_event( app_ui: &mut AppUi, key_track: &mut KeyTrack, is_running: Arc, -) -> Result, ApplicationError> { +) -> Result { match key_track.current_key().code { KeyCode::Down => { let (_, row) = app_ui.response.get_column_row(); if row == app_ui.response.max_row_idx() { // jump from response window to prompt window - return Ok(Some(app_ui.set_prompt_window(true))); + return Ok(app_ui.set_prompt_window(true)); } } KeyCode::Tab => { - return Ok(Some(app_ui.set_prompt_window(false))); + return Ok(app_ui.set_prompt_window(false)); } KeyCode::Char(key) => { // catch Ctrl + shortcut key if key_track.current_key().modifiers == KeyModifiers::CONTROL { match key { 'c' => { - return Ok(Some(WindowEvent::Quit)); + return Ok(WindowEvent::Quit); } 'q' => { - return Ok(Some(WindowEvent::Quit)); + return Ok(WindowEvent::Quit); } 'a' => { app_ui.response.text_select_all(); } _ => {} } - return Ok(Some(WindowEvent::ResponseWindow)); + return Ok(WindowEvent::ResponseWindow); } else { // process regular key match key { 'i' | 'I' => { - return Ok(Some(app_ui.set_prompt_window(true))); + return Ok(app_ui.set_prompt_window(true)); } 't' | 'T' => { - return Ok(Some(app_ui.set_prompt_window(false))); + return Ok(app_ui.set_prompt_window(false)); } '+' => { app_ui.set_primary_window(WindowKind::ResponseWindow); @@ -60,9 +60,7 @@ pub fn handle_response_window_event( if let Some(prev) = key_track.previous_key_str() { if prev == " " { // change to insert mode if double space - return Ok(Some( - app_ui.set_prompt_window(true), - )); + return Ok(app_ui.set_prompt_window(true)); } } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs index 4863ab5..1a69afa 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs @@ -7,8 +7,8 @@ use super::handle_command_line::handle_command_line_event; use super::handle_prompt_window::handle_prompt_window_event; use super::handle_response_window::handle_response_window_event; use super::{ - AppUi, ApplicationError, ConversationDbHandler, ThreadedChatSession, - WindowEvent, + AppUi, ApplicationError, ConversationDbHandler, ModalAction, + ThreadedChatSession, WindowEvent, }; #[derive(Debug, Clone)] @@ -162,7 +162,7 @@ impl KeyEventHandler { current_mode: WindowEvent, is_running: Arc, handler: &mut ConversationDbHandler, - ) -> Result, ApplicationError> { + ) -> Result { if !self.key_track.leader_key_set() || self .key_track @@ -190,13 +190,13 @@ impl KeyEventHandler { &mut self.key_track, is_running, ), - WindowEvent::Modal(window_type) => { + WindowEvent::Modal(_) => { // catch forced quit key events before passing control to modal let current_key = self.key_track.current_key(); if current_key.modifiers == KeyModifiers::CONTROL { match current_key.code { KeyCode::Char('c') | KeyCode::Char('q') => { - return Ok(Some(WindowEvent::Quit)); + return Ok(WindowEvent::Quit); } _ => {} } @@ -211,17 +211,11 @@ impl KeyEventHandler { ) .await { - Ok(Some(WindowEvent::Modal(next_window_type))) => { - if next_window_type == window_type { - // window remains un-changed - return Ok(Some(WindowEvent::Modal( - window_type, - ))); - } - WindowEvent::Modal(next_window_type) + Ok(WindowEvent::Modal(action)) => { + // pass as-is + WindowEvent::Modal(action) } - Ok(Some(new_window_event)) => new_window_event, - Ok(None) => WindowEvent::PromptWindow(None), // default + Ok(new_window_event) => new_window_event, Err(modal_error) => { match modal_error { ApplicationError::NotReady(message) => { @@ -231,9 +225,9 @@ impl KeyEventHandler { "Not Ready: {}", message ))?; - return Ok(Some(WindowEvent::Modal( - window_type, - ))); + return Ok(WindowEvent::Modal( + ModalAction::WaitForKeyEvent, + )); } _ => { log::error!( @@ -245,12 +239,12 @@ impl KeyEventHandler { } } }; - return Ok(Some(new_window_event)); + return Ok(new_window_event); } else { - Ok(Some(WindowEvent::Modal(window_type))) + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) } } - _ => Ok(Some(current_mode)), + _ => Ok(current_mode), } } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/leader_key.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/leader_key.rs index 92f995f..c2c355d 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/leader_key.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/leader_key.rs @@ -1,5 +1,5 @@ use super::key_event::KeyTrack; -use super::{ModalWindowType, WindowEvent}; +use super::{ModalAction, ModalWindowType, WindowEvent}; pub const LEADER_KEY: char = ' '; @@ -52,12 +52,12 @@ pub fn process_leader_key(key_track: &mut KeyTrack) -> Option { MatchOutcome::FullMatch(cmd) => { // NOTE: should match define_commands! macro let window_event = match cmd.as_str() { - "pe" => { - Some(WindowEvent::Modal(ModalWindowType::ProfileEdit)) - } - "pc" => Some(WindowEvent::Modal( - ModalWindowType::ConversationList(None), - )), + "pe" => Some(WindowEvent::Modal(ModalAction::Open( + ModalWindowType::ProfileEdit, + ))), + "pc" => Some(WindowEvent::Modal(ModalAction::Open( + ModalWindowType::ConversationList, + ))), _ => None, }; key_track.set_leader_key(false); diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs index 5dc284a..6e7285e 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs @@ -9,7 +9,7 @@ pub use key_event::{KeyEventHandler, KeyTrack}; use lumni::api::error::ApplicationError; use super::clipboard::ClipboardProvider; -use super::modals::ModalWindowType; +use super::modals::{ModalAction, ModalWindowType}; use super::ui::AppUi; use super::window::{ LineType, MoveCursor, PromptWindow, TextDocumentTrait, TextWindowTrait, @@ -18,14 +18,14 @@ use super::window::{ use super::{ConversationDbHandler, NewConversation, ThreadedChatSession}; pub use crate::external as lumni; -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq)] pub enum WindowEvent { Quit, PromptWindow(Option), ResponseWindow, CommandLine(Option), Prompt(PromptAction), - Modal(ModalWindowType), + Modal(ModalAction), } #[derive(Debug, Clone, PartialEq)] @@ -44,3 +44,11 @@ pub enum ConversationEvent { NewConversation(NewConversation), ReloadConversation, // only reload conversation } + +#[derive(Debug, Clone, PartialEq)] +pub enum UserEvent { + NewConversation(NewConversation), // prepare for future conversation + ReloadConversation, // reload conversation + NewProfile, // prepare for future profile switch + ReloadProfile, // prepare for future profile switch +} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/text_window_event.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/text_window_event.rs index 5261cc3..def4e39 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/text_window_event.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/text_window_event.rs @@ -16,7 +16,7 @@ pub fn handle_text_window_event<'a, T, D>( key_track: &mut KeyTrack, window: &mut T, _is_running: Arc, -) -> Result, ApplicationError> +) -> Result where T: TextWindowTrait<'a, D>, D: TextDocumentTrait, @@ -35,9 +35,8 @@ where key_track.set_leader_key(true); // enable leader key capture } else if key_track.leader_key_set() { // process captured leader key string - let window_event = process_leader_key(key_track); - if window_event.is_some() { - return Ok(window_event); + if let Some(event) = process_leader_key(key_track) { + return Ok(event); } } else { // process regular key @@ -104,14 +103,14 @@ where WindowKind::PromptWindow => WindowEvent::PromptWindow(None), WindowKind::CommandLine => WindowEvent::CommandLine(None), }; - Ok(Some(kind)) + Ok(kind) } fn handle_char_key<'a, T, D>( character: char, key_track: &mut KeyTrack, window: &mut T, -) -> Result, ApplicationError> +) -> Result where T: TextWindowTrait<'a, D>, D: TextDocumentTrait, @@ -199,9 +198,9 @@ where } ':' => { // Switch to command line mode on ":" key press - return Ok(Some(WindowEvent::CommandLine(Some( + return Ok(WindowEvent::CommandLine(Some( CommandLineAction::Write(":".to_string()), - )))); + ))); } // ignore other characters _ => {} @@ -211,7 +210,7 @@ where WindowKind::PromptWindow => WindowEvent::PromptWindow(None), WindowKind::CommandLine => WindowEvent::CommandLine(None), }; - Ok(Some(kind)) + Ok(kind) } fn yank_text<'a, T, D>(window: &mut T, lines_to_yank: Option) diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs index 5feda1b..a2c6b12 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs @@ -10,13 +10,13 @@ pub use colorscheme::{ColorScheme, ColorSchemeType}; pub use draw::draw_ui; pub use events::{ CommandLineAction, ConversationEvent, KeyEventHandler, KeyTrack, - PromptAction, WindowEvent, + PromptAction, UserEvent, WindowEvent, }; use lumni::api::error::ApplicationError; -pub use modals::{ModalWindowTrait, ModalWindowType}; +pub use modals::{ModalAction, ModalWindowTrait, ModalWindowType}; pub use ui::AppUi; pub use window::{ - CommandLine, PromptWindow, ResponseWindow, Scroller, TextLine, TextSegment, + CommandLine, PromptWindow, ResponseWindow, TextLine, TextSegment, TextWindowTrait, WindowKind, }; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/handle_key_event.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/handle_key_event.rs index 7fcd30e..62bf852 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/handle_key_event.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/handle_key_event.rs @@ -5,7 +5,7 @@ impl<'a> ConversationListModal<'a> { &mut self, tab_chat: &mut ThreadedChatSession, db_handler: &mut ConversationDbHandler, - ) -> Result, ApplicationError> { + ) -> Result { match self.current_tab { ConversationStatus::Deleted => { self.undo_delete_and_load_conversation(tab_chat, db_handler) @@ -13,16 +13,16 @@ impl<'a> ConversationListModal<'a> { } _ => self.load_and_set_conversation(tab_chat, db_handler).await?, } - Ok(Some(WindowEvent::Modal(ModalWindowType::ConversationList( - Some(ConversationEvent::ReloadConversation), - )))) + Ok(WindowEvent::Modal(ModalAction::Event( + UserEvent::ReloadConversation, + ))) } pub async fn handle_edit_mode_key_event( &mut self, key_event: &mut KeyTrack, handler: &mut ConversationDbHandler, - ) -> Result, ApplicationError> { + ) -> Result { match key_event.current_key().code { KeyCode::Enter => self.save_edited_name(handler).await?, KeyCode::Esc => self.cancel_edit_mode(), @@ -32,9 +32,7 @@ impl<'a> ConversationListModal<'a> { } } } - Ok(Some(WindowEvent::Modal(ModalWindowType::ConversationList( - None, - )))) + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) } pub async fn handle_normal_mode_key_event( @@ -42,7 +40,7 @@ impl<'a> ConversationListModal<'a> { key_event: &mut KeyTrack, tab_chat: &mut ThreadedChatSession, db_handler: &mut ConversationDbHandler, - ) -> Result, ApplicationError> { + ) -> Result { match key_event.current_key().code { KeyCode::Up => { self.move_selection_up(); @@ -55,10 +53,10 @@ impl<'a> ConversationListModal<'a> { return self.reload_conversation(tab_chat, db_handler).await; } KeyCode::Enter => { - return Ok(Some(WindowEvent::PromptWindow(None))); + return Ok(WindowEvent::PromptWindow(None)); } KeyCode::Char('q') => { - return Ok(Some(WindowEvent::PromptWindow(None))); + return Ok(WindowEvent::PromptWindow(None)); } KeyCode::Tab => { self.switch_tab(); @@ -79,13 +77,11 @@ impl<'a> ConversationListModal<'a> { KeyCode::Char('e') | KeyCode::Char('E') => { self.edit_conversation_name().await? } - KeyCode::Esc => return Ok(Some(WindowEvent::PromptWindow(None))), + KeyCode::Esc => return Ok(WindowEvent::PromptWindow(None)), _ => {} } - // stay in the Modal window - Ok(Some(WindowEvent::Modal(ModalWindowType::ConversationList( - None, - )))) + // stay in the Modal window, waiting for next key event + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) } async fn save_edited_name( diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs index 37136cf..f0caab2 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs @@ -17,9 +17,9 @@ use ratatui::Frame; use super::{ ApplicationError, CommandLine, Conversation, ConversationDbHandler, - ConversationEvent, ConversationStatus, KeyTrack, ModalWindowTrait, + ConversationStatus, KeyTrack, ModalAction, ModalWindowTrait, ModalWindowType, PromptInstruction, TextWindowTrait, ThreadedChatSession, - WindowEvent, + UserEvent, WindowEvent, }; use crate::apps::builtin::llm::prompt::src::chat::db::ConversationId; pub use crate::external as lumni; @@ -290,7 +290,7 @@ impl<'a> ConversationListModal<'a> { #[async_trait] impl<'a> ModalWindowTrait for ConversationListModal<'a> { fn get_type(&self) -> ModalWindowType { - ModalWindowType::ConversationList(None) + ModalWindowType::ConversationList } fn render_on_frame(&mut self, frame: &mut Frame, mut area: Rect) { @@ -320,7 +320,7 @@ impl<'a> ModalWindowTrait for ConversationListModal<'a> { key_event: &'b mut KeyTrack, tab_chat: &'b mut ThreadedChatSession, handler: &mut ConversationDbHandler, - ) -> Result, ApplicationError> { + ) -> Result { log::debug!( "Key: {:?}, Modifiers: {:?}", key_event.current_key().code, diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs index 3ba12a8..85f10a8 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs @@ -9,25 +9,39 @@ use ratatui::Frame; use super::{ ApplicationError, CommandLine, Conversation, ConversationDbHandler, - ConversationEvent, ConversationStatus, KeyTrack, MaskMode, - PromptInstruction, TextWindowTrait, ThreadedChatSession, - UserProfileDbHandler, WindowEvent, + ConversationStatus, KeyTrack, MaskMode, PromptInstruction, TextWindowTrait, + ThreadedChatSession, UserEvent, UserProfileDbHandler, WindowEvent, }; #[derive(Debug, Clone, PartialEq)] pub enum ModalWindowType { - ConversationList(Option), + ConversationList, ProfileEdit, } +#[derive(Debug, Clone, PartialEq)] +pub enum ModalAction { + Open(ModalWindowType), // open the modal + Refresh, // modal needs to be refreshed to handle updates + WaitForKeyEvent, // wait for a key event + Close, // close the curren modal + Event(UserEvent), +} + #[async_trait] -pub trait ModalWindowTrait { +pub trait ModalWindowTrait: Send + Sync { fn get_type(&self) -> ModalWindowType; fn render_on_frame(&mut self, frame: &mut Frame, area: Rect); + async fn refresh(&mut self) -> Result { + // handle_key_event can return WindowEvent::Modal(ModalAction::Refresh), + // which typically means a background process started, and can be monitored by calling refresh. Refresh can also return a WindowEvent with Action::Refresh. When background processes is completed, or none are running, it should return the default WaitForKeyEvent. + // Default implementation completes immediately + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } async fn handle_key_event<'a>( &'a mut self, key_event: &'a mut KeyTrack, tab_chat: &'a mut ThreadedChatSession, handler: &mut ConversationDbHandler, - ) -> Result, ApplicationError>; + ) -> Result; } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/mod.rs index 7539784..913dc04 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/mod.rs @@ -1,19 +1,21 @@ +use std::sync::Arc; +use std::time::Instant; + use async_trait::async_trait; -use crossterm::event::{KeyCode, KeyEvent}; -pub use lumni::Timestamp; -use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect}; +use crossterm::event::KeyCode; +use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{ - Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, - Scrollbar, ScrollbarOrientation, ScrollbarState, Tabs, + Block, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph, }; use ratatui::Frame; use serde_json::{json, Map, Value}; +use tokio::sync::{mpsc, Mutex}; use super::{ - ApplicationError, ConversationDbHandler, KeyTrack, MaskMode, - ModalWindowTrait, ModalWindowType, TextWindowTrait, ThreadedChatSession, + ApplicationError, ConversationDbHandler, KeyTrack, MaskMode, ModalAction, + ModalWindowTrait, ModalWindowType, ThreadedChatSession, UserProfileDbHandler, WindowEvent, }; pub use crate::external as lumni; @@ -32,8 +34,15 @@ pub struct ProfileEditModal { show_secure: bool, predefined_types: Vec, selected_type: usize, + background_task: Option>, + task_start_time: Option, + spinner_state: usize, + new_profile_name: Option, } +enum BackgroundTaskResult { + ProfileCreated(Result<(), ApplicationError>), +} enum Focus { ProfileList, SettingsList, @@ -86,6 +95,10 @@ impl ProfileEditModal { show_secure: false, predefined_types, selected_type: 0, + background_task: None, + task_start_time: None, + spinner_state: 0, + new_profile_name: None, }) } @@ -397,7 +410,7 @@ impl ProfileEditModal { async fn create_new_profile(&mut self) -> Result<(), ApplicationError> { let new_profile_name = format!("New_Profile_{}", self.profiles.len() + 1); - let profile_type = &self.predefined_types[self.selected_type]; + let profile_type = self.predefined_types[self.selected_type].clone(); let mut settings = Map::new(); settings.insert( @@ -405,7 +418,7 @@ impl ProfileEditModal { Value::String(profile_type.clone()), ); - // Add any default settings for the chosen profile type + // Add default settings based on the profile type match profile_type.as_str() { "OpenAI" => { settings.insert( @@ -427,21 +440,67 @@ impl ProfileEditModal { Value::String("claude-2".to_string()), ); } - _ => {} + "Custom" => { + // No default settings for custom profiles + } + _ => { + // Handle unexpected profile types + return Err(ApplicationError::InvalidInput( + "Unknown profile type".to_string(), + )); + } } - self.db_handler - .create_or_update(&new_profile_name, &Value::Object(settings)) - .await?; - - self.profiles.push(new_profile_name); - self.selected_profile = self.profiles.len() - 1; - self.load_profile().await?; - self.edit_mode = EditMode::NotEditing; - self.focus = Focus::SettingsList; + let db_handler = Arc::new(Mutex::new(self.db_handler.clone())); + let (tx, rx) = mpsc::channel(1); + + let new_profile_name_clone = new_profile_name.clone(); + tokio::spawn(async move { + let result = db_handler + .lock() + .await + .create_or_update( + &new_profile_name_clone, + &Value::Object(settings), + ) + .await; + let _ = tx.send(BackgroundTaskResult::ProfileCreated(result)).await; + }); + + self.background_task = Some(rx); + self.task_start_time = Some(std::time::Instant::now()); + self.spinner_state = 0; + self.edit_mode = EditMode::CreatingNewProfile; + self.focus = Focus::NewProfileType; + self.new_profile_name = Some(new_profile_name); Ok(()) } + + fn render_activity_indicator(&mut self, frame: &mut Frame, area: Rect) { + const SPINNER: &[char] = + &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + + let spinner_char = SPINNER[self.spinner_state]; + self.spinner_state = (self.spinner_state + 1) % SPINNER.len(); + + let elapsed = self + .task_start_time + .map(|start| start.elapsed().as_secs()) + .unwrap_or(0); + let content = format!( + "{} Creating profile... ({} seconds)", + spinner_char, elapsed + ); + + let paragraph = Paragraph::new(content) + .style(Style::default().fg(Color::Cyan)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + + frame.render_widget(paragraph, area); + } + fn start_adding_new_value(&mut self, is_secure: bool) { self.edit_mode = EditMode::AddingNewKey; self.new_key_buffer.clear(); @@ -562,7 +621,7 @@ impl ModalWindowTrait for ProfileEditModal { fn render_on_frame(&mut self, frame: &mut Frame, area: Rect) { frame.render_widget(Clear, area); - let chunks = Layout::default() + let main_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), @@ -571,24 +630,84 @@ impl ModalWindowTrait for ProfileEditModal { ]) .split(area); - let main_chunks = Layout::default() + let title = Paragraph::new("Profile Editor") + .style(Style::default().fg(Color::Cyan)) + .alignment(Alignment::Center); + frame.render_widget(title, main_chunks[0]); + + let content_area = main_chunks[1]; + + let content_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage(30), Constraint::Percentage(70), ]) - .split(chunks[1]); + .split(content_area); - self.render_profile_list(frame, main_chunks[0]); + self.render_profile_list(frame, content_chunks[0]); match self.edit_mode { EditMode::CreatingNewProfile => { - self.render_new_profile_type(frame, main_chunks[1]) + self.render_new_profile_type(frame, content_chunks[1]) } - _ => self.render_settings_list(frame, main_chunks[1]), + _ => self.render_settings_list(frame, content_chunks[1]), } - self.render_instructions(frame, chunks[2]); + self.render_instructions(frame, main_chunks[2]); + + // Render activity indicator if a background task is running + if self.background_task.is_some() { + let indicator_area = Rect { + x: area.x + 10, + y: area.bottom() - 3, + width: area.width - 20, + height: 3, + }; + + self.render_activity_indicator(frame, indicator_area); + } + } + + async fn refresh(&mut self) -> Result { + if let Some(ref mut rx) = self.background_task { + match rx.try_recv() { + Ok(BackgroundTaskResult::ProfileCreated(result)) => { + self.background_task = None; + self.task_start_time = None; + match result { + Ok(()) => { + if let Some(new_profile_name) = + self.new_profile_name.take() + { + self.profiles.push(new_profile_name); + self.selected_profile = self.profiles.len() - 1; + self.load_profile().await?; + } + self.edit_mode = EditMode::NotEditing; + self.focus = Focus::SettingsList; + } + Err(e) => { + log::error!("Failed to create profile: {}", e); + } + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + Err(mpsc::error::TryRecvError::Empty) => { + // Task is still running, update will happen in render_activity_indicator + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + Err(mpsc::error::TryRecvError::Disconnected) => { + // Task has ended unexpectedly + self.background_task = None; + self.task_start_time = None; + self.new_profile_name = None; + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + } + } else { + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } } async fn handle_key_event<'b>( @@ -596,7 +715,7 @@ impl ModalWindowTrait for ProfileEditModal { key_event: &'b mut KeyTrack, _tab_chat: &'b mut ThreadedChatSession, _handler: &mut ConversationDbHandler, - ) -> Result, ApplicationError> { + ) -> Result { match (&self.focus, &self.edit_mode, key_event.current_key().code) { // Profile List Navigation and Actions (Focus::ProfileList, EditMode::NotEditing, KeyCode::Up) => { @@ -701,6 +820,7 @@ impl ModalWindowTrait for ProfileEditModal { KeyCode::Enter, ) => { self.create_new_profile().await?; + return Ok(WindowEvent::Modal(ModalAction::Refresh)); } // Settings List Navigation and Editing @@ -790,7 +910,7 @@ impl ModalWindowTrait for ProfileEditModal { // Global Escape Handling (_, _, KeyCode::Esc) => match self.edit_mode { EditMode::NotEditing => { - return Ok(Some(WindowEvent::PromptWindow(None))) + return Ok(WindowEvent::PromptWindow(None)) } EditMode::RenamingProfile => { self.edit_mode = EditMode::NotEditing; @@ -804,7 +924,7 @@ impl ModalWindowTrait for ProfileEditModal { _ => {} } - // Stay in the Modal window - Ok(Some(WindowEvent::Modal(ModalWindowType::ProfileEdit))) + // Stay in the Modal window, waiting for next key event + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs index 3b3e668..c13d400 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs @@ -53,7 +53,7 @@ impl AppUi<'_> { conversation_id: Option, ) -> Result<(), ApplicationError> { self.modal = match modal_type { - ModalWindowType::ConversationList(_) => { + ModalWindowType::ConversationList => { let handler = db_conn.get_conversation_handler(conversation_id); Some(Box::new(ConversationListModal::new(handler).await?)) }