diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/encryption/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/encryption/mod.rs index 7871aeef..440bf748 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/encryption/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/encryption/mod.rs @@ -377,7 +377,8 @@ impl EncryptionHandler { let handler = EncryptionHandler::new_from_path(private_key_path)? .ok_or_else(|| { ApplicationError::EncryptionError(EncryptionError::InvalidKey( - "No encryption handler available".to_string(), + "Encryption handler required to get private key hash" + .to_string(), )) })?; diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs index 3342f646..3ad4fefb 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs @@ -97,6 +97,27 @@ impl UserProfileDbHandler { .map_err(ApplicationError::from) } + pub async fn get_profile_list( + &self, + ) -> Result, ApplicationError> { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + let mut stmt = + tx.prepare("SELECT name FROM user_profiles ORDER BY name ASC")?; + let profiles = stmt + .query_map([], |row| row.get(0))? + .collect::, _>>() + .map_err(|e| DatabaseOperationError::SqliteError(e))?; + Ok(profiles) + }) + .map_err(|e| match e { + DatabaseOperationError::SqliteError(sqlite_err) => { + ApplicationError::DatabaseError(sqlite_err.to_string()) + } + DatabaseOperationError::ApplicationError(app_err) => app_err, + }) + } + pub async fn register_encryption_key( &self, name: &str, diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/encryption_operations.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/encryption_operations.rs index dc4418b7..7c17f152 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/encryption_operations.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/encryption_operations.rs @@ -26,7 +26,7 @@ impl UserProfileDbHandler { "encryption_key": encryption_key, })) } else { - eprintln!("No encryption handler available"); + eprintln!("Encryption handler required to encrypt value"); Ok(JsonValue::String(content.to_string())) } } @@ -79,7 +79,7 @@ impl UserProfileDbHandler { } else { Err(ApplicationError::EncryptionError( EncryptionError::InvalidKey( - "No encryption handler available".to_string(), + "Encryption handler required to decrypt value".to_string(), ), )) } @@ -114,7 +114,7 @@ impl UserProfileDbHandler { } else { Err(ApplicationError::EncryptionError( EncryptionError::InvalidKey( - "No encryption handler available".to_string(), + "Encryption handler required to validate hash".to_string(), ), )) } 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 094c29c2..2bef9064 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 @@ -20,13 +20,13 @@ pub struct UserProfileDbHandler { encryption_handler: Option>, } -#[derive(PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum EncryptionMode { Encrypt, Decrypt, } -#[derive(PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum MaskMode { Mask, Unmask, @@ -56,7 +56,9 @@ impl UserProfileDbHandler { // If profile is not yet set, return error as we need to know the profile to validate against existing encryption handler let profile_name = self.profile_name.as_ref().ok_or_else(|| { ApplicationError::InvalidInput( - "Profile name must be defined before setting encryption handler".to_string(), + "Profile name must be defined before setting encryption \ + handler" + .to_string(), ) })?; @@ -117,6 +119,15 @@ impl UserProfileDbHandler { Ok(()) } + pub fn set_profile_with_encryption_handler( + &mut self, + profile_name: String, + encryption_handler: Arc, + ) -> Result<(), ApplicationError> { + self.set_profile_name(profile_name); + self.set_encryption_handler(encryption_handler) + } + pub async fn export_profile_settings( &mut self, profile_name: &str, diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs index 5e188b61..4f2af32e 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs @@ -108,7 +108,10 @@ impl UserProfileDbHandler { // Set new encryption handler outside the closure if let Some(new_encryption_handler) = created_encryption_handler { // use method as it protects against overwriting existing encryption configuration - self.set_encryption_handler(Arc::new(new_encryption_handler))?; + self.set_profile_with_encryption_handler( + profile_name.to_string(), + Arc::new(new_encryption_handler), + )?; } else { return Err(ApplicationError::InvalidInput( "Failed to create encryption handler".to_string(), @@ -188,6 +191,11 @@ impl UserProfileDbHandler { profile_name: &str, mask_mode: MaskMode, ) -> Result { + log::debug!( + "Getting settings for profile: {} ({:?})", + profile_name, + mask_mode + ); let (json_string, key_hash, key_path): (String, String, String) = { let mut db = self.db.lock().await; db.process_queue_with_result(|tx| { @@ -204,8 +212,8 @@ impl UserProfileDbHandler { .map_err(DatabaseOperationError::SqliteError) })? }; - self.profile_name = Some(profile_name.to_string()); - if self.encryption_handler.is_none() { + if mask_mode == MaskMode::Unmask && self.encryption_handler.is_none() { + // encryption handled required for decryption let encryption_handler = EncryptionHandler::new_from_path(&PathBuf::from(&key_path))? .ok_or_else(|| { @@ -213,18 +221,22 @@ impl UserProfileDbHandler { "Failed to load encryption handler".to_string(), ) })?; - // use method as it protects against overwriting existing encryption configuration - self.set_encryption_handler(Arc::new(encryption_handler))?; + self.set_profile_with_encryption_handler( + profile_name.to_string(), + Arc::new(encryption_handler), + )?; + self.verify_encryption_key_hash(&key_hash)?; } - self.verify_encryption_key_hash(&key_hash)?; let settings: JsonValue = serde_json::from_str(&json_string).map_err(|e| { ApplicationError::InvalidInput(format!("Invalid JSON: {}", e)) })?; - self.process_settings_with_metadata( + let r = self.process_settings_with_metadata( &settings, EncryptionMode::Decrypt, mask_mode, - ) + ); + eprintln!("get_profile_settings: {:?}", r); + r } } 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 0ae76871..62009237 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 @@ -55,7 +55,7 @@ pub fn handle_command_line_event( // double-colon opens Modal (Config) window app_ui.command_line.text_empty(); app_ui.command_line.set_status_inactive(); - Ok(Some(WindowEvent::Modal(ModalWindowType::SelectEndpoint))) + Ok(Some(WindowEvent::Modal(ModalWindowType::ProfileEdit))) } _ => handle_text_window_event( key_track, 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 c4093604..92f995f1 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 @@ -52,9 +52,9 @@ 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, - )), + "pe" => { + Some(WindowEvent::Modal(ModalWindowType::ProfileEdit)) + } "pc" => Some(WindowEvent::Modal( ModalWindowType::ConversationList(None), )), diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/endpoint/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/endpoint/mod.rs deleted file mode 100644 index 946a688f..00000000 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/endpoint/mod.rs +++ /dev/null @@ -1,120 +0,0 @@ -mod select; - -use async_trait::async_trait; -use crossterm::event::KeyCode; -use ratatui::layout::Rect; -use ratatui::widgets::Clear; -use ratatui::Frame; -use select::SelectEndpoint; - -use super::{ - ApplicationError, ConversationDbHandler, ConversationEvent, KeyTrack, - ModalWindowTrait, ModalWindowType, ModelServer, NewConversation, Scroller, - ServerManager, ServerTrait, ThreadedChatSession, WindowEvent, - SUPPORTED_MODEL_ENDPOINTS, -}; - -pub struct SelectEndpointModal { - widget: SelectEndpoint, - _scroller: Option, -} - -impl SelectEndpointModal { - pub fn new() -> Self { - Self { - widget: SelectEndpoint::new(), - _scroller: None, - } - } -} - -#[async_trait] -impl ModalWindowTrait for SelectEndpointModal { - fn get_type(&self) -> ModalWindowType { - ModalWindowType::SelectEndpoint - } - - fn render_on_frame(&mut self, frame: &mut Frame, mut area: Rect) { - let (max_width, max_height) = self.widget.max_area_size(); - if area.width > max_width { - area.x = area.width.saturating_sub(max_width); - area.width = max_width; - }; - if area.height > max_height { - area.height = max_height; - }; - frame.render_widget(Clear, area); - frame.render_widget(&mut self.widget, area); - } - - async fn handle_key_event<'a>( - &'a mut self, - key_event: &'a mut KeyTrack, - tab_chat: &'a mut ThreadedChatSession, - handler: &mut ConversationDbHandler, - ) -> Result, ApplicationError> { - match key_event.current_key().code { - KeyCode::Up => self.widget.key_up(), - KeyCode::Down => self.widget.key_down(), - KeyCode::Enter => { - let selected_server = self.widget.current_endpoint(); - // TODO: allow model selection, + check if model changes - // TODO: catch ApplicationError::NotReady, if it is assume selected_server != tab_chat.server_name() - let instruction = tab_chat.get_instruction().await?; - let server_name = instruction - .get_completion_options() - .model_server - .as_ref() - .map(|s| s.to_string()); - - let should_create_new_conversation = match server_name { - Some(current_server_name) => { - selected_server != current_server_name - } - None => true, // Assume new server if no current server - }; - - let event = if should_create_new_conversation { - let server = ModelServer::from_str(selected_server)?; - match server.get_default_model().await { - Ok(model) => { - let new_conversation = NewConversation::new( - server.server_name(), - model, - &handler, - ) - .await?; - // Return the new conversation event - Ok(Some(WindowEvent::PromptWindow(Some( - ConversationEvent::NewConversation( - new_conversation, - ), - )))) - } - Err(ApplicationError::NotReady(e)) => { - // already a NotReady error - Err(ApplicationError::NotReady(e)) - } - Err(e) => { - // ensure each error is converted to NotReady, - // with additional logging as its unexpected - log::error!("Error: {}", e); - Err(ApplicationError::NotReady(e.to_string())) - } - } - } else { - Ok(Some(WindowEvent::PromptWindow(None))) - }; - return event; - } - KeyCode::Left => { - let server = - ModelServer::from_str(self.widget.current_endpoint())?; - let _models = server.list_models().await?; - } - _ => {} // Ignore other keys - } - // stay in the modal window - Ok(Some(WindowEvent::Modal(ModalWindowType::SelectEndpoint))) - } -} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/endpoint/select.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/endpoint/select.rs deleted file mode 100644 index 6ee7aec2..00000000 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/endpoint/select.rs +++ /dev/null @@ -1,92 +0,0 @@ -use ratatui::buffer::Buffer; -use ratatui::layout::{Constraint, Direction, Layout, Rect}; -use ratatui::style::{Color, Modifier, Style}; -use ratatui::text::Line; -use ratatui::widgets::block::{Block, Padding}; -use ratatui::widgets::{List, ListItem, Widget}; - -use super::SUPPORTED_MODEL_ENDPOINTS; - -const MAX_WIDTH: u16 = 20; -const MAX_HEIGHT: u16 = 8; - -pub struct SelectEndpoint { - current_index: usize, // Current selection index -} - -impl SelectEndpoint { - pub fn new() -> Self { - Self { current_index: 0 } // Initialize with the first item selected - } - - pub fn max_area_size(&self) -> (u16, u16) { - (MAX_WIDTH, MAX_HEIGHT) - } - - pub fn key_down(&mut self) { - // Increment the current index, wrapping around to 0 if past the last item - self.current_index = - (self.current_index + 1) % SUPPORTED_MODEL_ENDPOINTS.len(); - } - - pub fn key_up(&mut self) { - self.current_index = if self.current_index == 0 { - // get the last index if the current index is 0 - SUPPORTED_MODEL_ENDPOINTS.len().saturating_sub(1) - } else { - self.current_index.saturating_sub(1) - }; - } - - pub fn current_index(&self) -> usize { - self.current_index - } - - pub fn current_endpoint(&self) -> &str { - SUPPORTED_MODEL_ENDPOINTS[self.current_index] - } -} - -impl Widget for &mut SelectEndpoint { - fn render(self, area: Rect, buf: &mut Buffer) { - // Define the layout: a line of text and a list below it - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([ - Constraint::Min(4), // Space for the list - ]) - .split(area); - - let background_block = Block::default() - .borders(ratatui::widgets::Borders::ALL) - .title("Select Endpoint") - .style(Style::default().bg(Color::Blue)); // Set the background color here - - background_block.render(area, buf); // Render the background block first - - // Prepare and render the list in the second chunk - let items: Vec = SUPPORTED_MODEL_ENDPOINTS - .iter() - .enumerate() - .map(|(index, &endpoint)| { - let style = if index == self.current_index { - Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Yellow) // Highlighted style - } else { - Style::default().fg(Color::White) // Normal style - }; - ListItem::new(Line::styled(endpoint, style)) - }) - .collect(); - - let list = List::new(items).block( - Block::default() - .padding(Padding::uniform(1)) - .style(Style::default().bg(Color::Black)) - .borders(ratatui::widgets::Borders::NONE), - ); - list.render(chunks[0], buf); - } -} 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 c991fa09..3ba12a8b 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 @@ -1,25 +1,21 @@ mod conversations; -mod endpoint; mod profiles; use async_trait::async_trait; pub use conversations::ConversationListModal; -pub use endpoint::SelectEndpointModal; pub use profiles::ProfileEditModal; use ratatui::layout::Rect; use ratatui::Frame; use super::{ ApplicationError, CommandLine, Conversation, ConversationDbHandler, - ConversationEvent, ConversationStatus, KeyTrack, MaskMode, ModelServer, - NewConversation, PromptInstruction, Scroller, ServerManager, ServerTrait, - TextWindowTrait, ThreadedChatSession, UserProfileDbHandler, WindowEvent, - SUPPORTED_MODEL_ENDPOINTS, + ConversationEvent, ConversationStatus, KeyTrack, MaskMode, + PromptInstruction, TextWindowTrait, ThreadedChatSession, + UserProfileDbHandler, WindowEvent, }; #[derive(Debug, Clone, PartialEq)] pub enum ModalWindowType { - SelectEndpoint, ConversationList(Option), ProfileEdit, } 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 2d1b885d..fc213416 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 @@ -12,47 +12,85 @@ use ratatui::Frame; use serde_json::Value; use super::{ - ApplicationError, ConversationDbHandler, - KeyTrack, MaskMode, - ModalWindowTrait, ModalWindowType, TextWindowTrait, - ThreadedChatSession, UserProfileDbHandler, WindowEvent, + ApplicationError, ConversationDbHandler, KeyTrack, MaskMode, + ModalWindowTrait, ModalWindowType, TextWindowTrait, ThreadedChatSession, + UserProfileDbHandler, WindowEvent, }; pub use crate::external as lumni; pub struct ProfileEditModal { + profiles: Vec, + selected_profile: usize, settings: Value, current_field: usize, editing: bool, edit_buffer: String, db_handler: UserProfileDbHandler, + focus: Focus, + show_secure: bool, +} + +enum Focus { + ProfileList, + SettingsList, } impl ProfileEditModal { pub async fn new( mut db_handler: UserProfileDbHandler, ) -> Result { - eprintln!("db_handler: {:?}", db_handler); - let settings = db_handler - .get_profile_settings("foo", MaskMode::Unmask) - .await?; - eprintln!("settings: {:?}", settings); + let profiles = db_handler.get_profile_list().await?; + let selected_profile = 0; + let settings = if !profiles.is_empty() { + db_handler + .get_profile_settings(&profiles[0], MaskMode::Mask) + .await? + } else { + Value::Object(serde_json::Map::new()) + }; + Ok(Self { + profiles, + selected_profile, settings, current_field: 0, editing: false, edit_buffer: String::new(), db_handler, + focus: Focus::ProfileList, + show_secure: false, }) } - fn render_title(&self, f: &mut Frame, area: Rect) { - let title = Paragraph::new("Profile Edit") - .style(Style::default().fg(Color::Cyan)) - .block(Block::default().borders(Borders::ALL)); - f.render_widget(title, area); + fn render_profiles_list(&self, f: &mut Frame, area: Rect) { + let items: Vec = self + .profiles + .iter() + .enumerate() + .map(|(i, profile)| { + let style = if i == self.selected_profile + && matches!(self.focus, Focus::ProfileList) + { + Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White) + } else { + Style::default().bg(Color::Black).fg(Color::Cyan) + }; + ListItem::new(Line::from(vec![Span::styled(profile, style)])) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("Profiles")) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol(">> "); + + let mut state = ListState::default(); + state.select(Some(self.selected_profile)); + + f.render_stateful_widget(list, area, &mut state); } - fn render_settings_list(&mut self, f: &mut Frame, area: Rect) { + fn render_settings_list(&self, f: &mut Frame, area: Rect) { let items: Vec = self .settings .as_object() @@ -60,24 +98,52 @@ impl ProfileEditModal { .iter() .enumerate() .map(|(i, (key, value))| { - let content = if self.editing && i == self.current_field { + let is_editable = !key.starts_with("__"); + let is_secure = value.is_object() + && value.get("was_encrypted") == Some(&Value::Bool(true)); + let content = if self.editing + && i == self.current_field + && is_editable + { format!("{}: {}", key, self.edit_buffer) } else { - format!("{}: {}", key, value.as_str().unwrap_or("")) + let display_value = if is_secure { + if self.show_secure { + value["value"].as_str().unwrap_or("").to_string() + } else { + "*****".to_string() + } + } else { + value.as_str().unwrap_or("").to_string() + }; + let lock_icon = if is_secure { + if self.show_secure { + "🔓 " + } else { + "🔒 " + } + } else { + "" + }; + format!("{}{}: {}", lock_icon, key, display_value) }; - let style = if i == self.current_field { - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD) + let style = if i == self.current_field + && matches!(self.focus, Focus::SettingsList) + { + Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White) + } else if is_editable { + Style::default().bg(Color::Black).fg(Color::Cyan) } else { - Style::default() + Style::default().bg(Color::Black).fg(Color::DarkGray) }; ListItem::new(Line::from(vec![Span::styled(content, style)])) }) .collect(); let list = List::new(items) - .block(Block::default().borders(Borders::ALL).title("Settings")); + .block(Block::default().borders(Borders::ALL).title("Settings")) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol(">> "); let mut state = ListState::default(); state.select(Some(self.current_field)); @@ -85,17 +151,75 @@ impl ProfileEditModal { f.render_stateful_widget(list, area, &mut state); } - fn render_instructions(&self, f: &mut Frame, area: Rect) { - let instructions = if self.editing { - "Enter: Save | Esc: Cancel" + fn render_selected_profile(&self, f: &mut Frame, area: Rect) { + let profile = &self.profiles[self.selected_profile]; + let secure_status = if self.show_secure { + "Visible" } else { - "↑↓: Navigate | Enter: Edit | Esc: Close" + "Hidden" + }; + let content = vec![ + Line::from(vec![ + Span::styled("Name: ", Style::default().fg(Color::Cyan)), + Span::raw(profile), + ]), + Line::from(vec![ + Span::styled( + "Secure values: ", + Style::default().fg(Color::Cyan), + ), + Span::raw(secure_status), + ]), + ]; + let paragraph = Paragraph::new(content).block( + Block::default() + .borders(Borders::ALL) + .title("Selected Profile"), + ); + f.render_widget(paragraph, area); + } + + fn render_instructions(&self, f: &mut Frame, area: Rect) { + let instructions = match self.focus { + Focus::ProfileList => { + "↑↓: Navigate | Enter: Select | Tab: Settings | Esc: Close" + } + Focus::SettingsList => { + if self.editing { + "Enter: Save | Esc: Cancel" + } else { + "↑↓: Navigate | Enter: Edit | S: Show/Hide Secure | Tab: \ + Profiles | Esc: Close" + } + } }; let paragraph = Paragraph::new(instructions) .style(Style::default().fg(Color::Cyan)); f.render_widget(paragraph, area); } + async fn load_profile(&mut self) -> Result<(), ApplicationError> { + let profile = &self.profiles[self.selected_profile]; + let mask_mode = if self.show_secure { + MaskMode::Unmask + } else { + MaskMode::Mask + }; + self.settings = self + .db_handler + .get_profile_settings(profile, mask_mode) + .await?; + self.current_field = 0; + Ok(()) + } + + async fn toggle_secure_visibility( + &mut self, + ) -> Result<(), ApplicationError> { + self.show_secure = !self.show_secure; + self.load_profile().await + } + fn move_selection_up(&mut self) { if self.current_field > 0 { self.current_field -= 1; @@ -109,7 +233,6 @@ impl ProfileEditModal { } fn start_editing(&mut self) { - self.editing = true; let current_key = self .settings .as_object() @@ -117,10 +240,13 @@ impl ProfileEditModal { .keys() .nth(self.current_field) .unwrap(); - self.edit_buffer = self.settings[current_key] - .as_str() - .unwrap_or("") - .to_string(); + if !current_key.starts_with("__") { + self.editing = true; + self.edit_buffer = self.settings[current_key] + .as_str() + .unwrap_or("") + .to_string(); + } } async fn save_edit(&mut self) -> Result<(), ApplicationError> { @@ -133,10 +259,14 @@ impl ProfileEditModal { .nth(self.current_field) .unwrap() .to_string(); - self.settings[¤t_key] = Value::String(self.edit_buffer.clone()); - self.db_handler - .create_or_update("foo", &self.settings) - .await?; + if !current_key.starts_with("__") { + self.settings[¤t_key] = + Value::String(self.edit_buffer.clone()); + let profile = &self.profiles[self.selected_profile]; + self.db_handler + .create_or_update(profile, &self.settings) + .await?; + } Ok(()) } @@ -153,18 +283,28 @@ impl ModalWindowTrait for ProfileEditModal { } fn render_on_frame(&mut self, frame: &mut Frame, area: Rect) { + frame.render_widget(Clear, area); + let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), + Constraint::Length(3), // Reduced height for profile details Constraint::Min(1), - Constraint::Length(3), + Constraint::Length(1), ]) .split(area); - frame.render_widget(Clear, area); - self.render_title(frame, chunks[0]); - self.render_settings_list(frame, chunks[1]); + let main_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(30), + Constraint::Percentage(70), + ]) + .split(chunks[1]); + + self.render_selected_profile(frame, chunks[0]); + self.render_profiles_list(frame, main_chunks[0]); + self.render_settings_list(frame, main_chunks[1]); self.render_instructions(frame, chunks[2]); } @@ -174,19 +314,57 @@ impl ModalWindowTrait for ProfileEditModal { _tab_chat: &'b mut ThreadedChatSession, _handler: &mut ConversationDbHandler, ) -> Result, ApplicationError> { - match (self.editing, key_event.current_key().code) { - (false, KeyCode::Up) => self.move_selection_up(), - (false, KeyCode::Down) => self.move_selection_down(), - (false, KeyCode::Enter) => self.start_editing(), - (false, KeyCode::Esc) => { - return Ok(Some(WindowEvent::PromptWindow(None))) + match (&self.focus, self.editing, key_event.current_key().code) { + (Focus::ProfileList, _, KeyCode::Up) => { + if self.selected_profile > 0 { + self.selected_profile -= 1; + self.load_profile().await?; + } + } + (Focus::ProfileList, _, KeyCode::Down) => { + if self.selected_profile < self.profiles.len() - 1 { + self.selected_profile += 1; + self.load_profile().await?; + } + } + (Focus::ProfileList, _, KeyCode::Enter) => { + self.focus = Focus::SettingsList; + } + (Focus::ProfileList, _, KeyCode::Tab) => { + self.focus = Focus::SettingsList; + } + (Focus::SettingsList, false, KeyCode::Up) => { + self.move_selection_up() + } + (Focus::SettingsList, false, KeyCode::Down) => { + self.move_selection_down() } - (true, KeyCode::Enter) => self.save_edit().await?, - (true, KeyCode::Esc) => self.cancel_edit(), - (true, KeyCode::Char(c)) => self.edit_buffer.push(c), - (true, KeyCode::Backspace) => { + (Focus::SettingsList, false, KeyCode::Enter) => { + self.start_editing() + } + (Focus::SettingsList, false, KeyCode::Tab) => { + self.focus = Focus::ProfileList; + } + ( + Focus::SettingsList, + false, + KeyCode::Char('s') | KeyCode::Char('S'), + ) => { + self.toggle_secure_visibility().await?; + } + (Focus::SettingsList, true, KeyCode::Enter) => { + self.save_edit().await? + } + (Focus::SettingsList, true, KeyCode::Esc) => self.cancel_edit(), + (Focus::SettingsList, true, KeyCode::Char(c)) => { + self.edit_buffer.push(c) + } + (Focus::SettingsList, true, KeyCode::Backspace) => { self.edit_buffer.pop(); } + (_, _, KeyCode::Esc) => { + return Ok(Some(WindowEvent::PromptWindow(None))) + } _ => {} } Ok(Some(WindowEvent::Modal(ModalWindowType::ProfileEdit))) 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 9b43fb20..3b3e6682 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs @@ -2,15 +2,12 @@ use std::sync::Arc; use lumni::api::error::ApplicationError; -use super::modals::{ - ConversationListModal, ProfileEditModal, SelectEndpointModal, -}; +use super::modals::{ConversationListModal, ProfileEditModal}; use super::{ CommandLine, ConversationDatabase, ConversationId, ModalWindowTrait, ModalWindowType, PromptWindow, ResponseWindow, TextLine, TextWindowTrait, WindowEvent, WindowKind, }; -use crate::apps::builtin::llm::prompt::src::chat::db; pub use crate::external as lumni; pub struct AppUi<'a> { @@ -56,9 +53,6 @@ impl AppUi<'_> { conversation_id: Option, ) -> Result<(), ApplicationError> { self.modal = match modal_type { - ModalWindowType::SelectEndpoint => { - Some(Box::new(SelectEndpointModal::new())) - } ModalWindowType::ConversationList(_) => { let handler = db_conn.get_conversation_handler(conversation_id); Some(Box::new(ConversationListModal::new(handler).await?))