From 861e8b8cecdc5037a7111061d949194fe2c666fe Mon Sep 17 00:00:00 2001 From: Anthony Potappel Date: Fri, 30 Aug 2024 23:46:50 +0200 Subject: [PATCH] wip - refactor profile/ provider II --- .../builtin/llm/prompt/src/chat/db/mod.rs | 9 +- .../builtin/llm/prompt/src/chat/db/schema.sql | 2 - .../prompt/src/chat/db/user_profile/mod.rs | 27 +- .../chat/db/user_profile/provider_config.rs | 194 +++--- .../apps/builtin/llm/prompt/src/chat/mod.rs | 10 +- .../apps/builtin/llm/prompt/src/tui/mod.rs | 9 +- .../builtin/llm/prompt/src/tui/modals/mod.rs | 12 +- .../llm/prompt/src/tui/modals/profiles/mod.rs | 22 - .../tui/modals/profiles/profile_edit_modal.rs | 624 ------------------ .../modals/profiles/profile_edit_renderer.rs | 252 ------- .../tui/modals/profiles/settings_editor.rs | 447 ------------- .../llm/prompt/src/tui/modals/settings/mod.rs | 200 ++++++ .../tui/modals/settings/profile/creator.rs | 378 +++++++++++ .../profile/list.rs} | 105 ++- .../tui/modals/settings/profile/manager.rs | 450 +++++++++++++ .../src/tui/modals/settings/profile/mod.rs | 9 + .../tui/modals/settings/profile/renderer.rs | 266 ++++++++ .../provider/creator.rs} | 253 +------ .../src/tui/modals/settings/provider/list.rs | 89 +++ .../tui/modals/settings/provider/manager.rs | 399 +++++++++++ .../src/tui/modals/settings/provider/mod.rs | 10 + .../tui/modals/settings/provider/renderer.rs | 229 +++++++ .../tui/modals/settings/settings_editor.rs | 257 ++++++++ .../src/apps/builtin/llm/prompt/src/tui/ui.rs | 6 +- 24 files changed, 2515 insertions(+), 1744 deletions(-) delete mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/mod.rs delete mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_edit_modal.rs delete mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_edit_renderer.rs delete mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/settings_editor.rs create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/mod.rs create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/creator.rs rename lumni/src/apps/builtin/llm/prompt/src/tui/modals/{profiles/profile_list.rs => settings/profile/list.rs} (58%) create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/manager.rs create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/mod.rs create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/renderer.rs rename lumni/src/apps/builtin/llm/prompt/src/tui/modals/{profiles/provider_manager.rs => settings/provider/creator.rs} (76%) create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/list.rs create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/manager.rs create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/mod.rs create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/renderer.rs create mode 100644 lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/settings_editor.rs diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/mod.rs index 82c2e41..956b0f4 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/mod.rs @@ -15,12 +15,13 @@ pub use lumni::Timestamp; pub use model::{ModelIdentifier, ModelSpec}; use serde::{Deserialize, Serialize}; pub use store::ConversationDatabase; -pub use user_profile::{MaskMode, UserProfile, UserProfileDbHandler}; +pub use user_profile::{ + MaskMode, ProviderConfig, ProviderConfigOptions, UserProfile, + UserProfileDbHandler, +}; pub use super::ConversationCache; -use super::{ - AdditionalSetting, ModelBackend, ModelServer, PromptRole, ProviderConfig, -}; +use super::{ModelBackend, ModelServer, PromptRole}; use crate::external as lumni; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/schema.sql b/lumni/src/apps/builtin/llm/prompt/src/chat/db/schema.sql index 02c68aa..44d26d1 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/schema.sql +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/schema.sql @@ -10,10 +10,8 @@ CREATE TABLE user_profiles ( options TEXT NOT NULL, -- JSON string is_default INTEGER DEFAULT 0, encryption_key_id INTEGER NOT NULL, - provider_config_id INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (encryption_key_id) REFERENCES encryption_keys(id) - FOREIGN KEY (provider_config_id) REFERENCES provider_configs(id) ); CREATE TABLE provider_configs ( 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 a2ee936..6445ef9 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 @@ -3,18 +3,19 @@ mod database_operations; mod encryption_operations; mod profile_operations; mod provider_config; + +use std::collections::HashMap; use std::sync::Arc; use lumni::api::error::{ApplicationError, EncryptionError}; use rusqlite::{params, OptionalExtension}; -use serde_json::{json, Value as JsonValue}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; use tokio::sync::Mutex as TokioMutex; use super::connector::{DatabaseConnector, DatabaseOperationError}; use super::encryption::EncryptionHandler; -use super::{ - AdditionalSetting, ModelBackend, ModelServer, ModelSpec, ProviderConfig, -}; +use super::{ModelBackend, ModelServer, ModelSpec}; use crate::external as lumni; #[derive(Debug, Clone, PartialEq)] @@ -30,6 +31,24 @@ pub struct UserProfileDbHandler { encryption_handler: Option>, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderConfig { + pub id: Option, + pub name: String, + pub provider_type: String, + pub model_identifier: Option, + pub additional_settings: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderConfigOptions { + pub name: String, + pub display_name: String, + pub value: String, + pub is_secure: bool, + pub placeholder: String, +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum EncryptionMode { Encrypt, diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/provider_config.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/provider_config.rs index cead2e9..aa41180 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/provider_config.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/provider_config.rs @@ -7,44 +7,37 @@ impl UserProfileDbHandler { pub async fn save_provider_config( &mut self, config: &ProviderConfig, - ) -> Result<(), ApplicationError> { + ) -> Result { let encryption_key_id = self.get_or_create_encryption_key().await?; - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - let additional_settings: HashMap = config - .additional_settings - .iter() - .map(|(k, v)| (k.clone(), v.value.clone())) - .collect(); - - let additional_settings_json = - serde_json::to_string(&additional_settings).map_err(|e| { - DatabaseOperationError::ApplicationError( - ApplicationError::InvalidInput(format!( - "Failed to serialize additional settings: {}", - e - )), - ) - })?; + let additional_settings_json = serde_json::to_string( + &config.additional_settings, + ) + .map_err(|e| { + DatabaseOperationError::ApplicationError( + ApplicationError::InvalidInput(format!( + "Failed to serialize additional settings: {}", + e + )), + ) + })?; - // Use encrypt_value method to encrypt the settings let encrypted_value = self .encrypt_value(&JsonValue::String(additional_settings_json)) .map_err(DatabaseOperationError::ApplicationError)?; - // Extract the encrypted content - let encrypted_settings = - encrypted_value["content"].as_str().ok_or_else(|| { + let encrypted_value_json = serde_json::to_string(&encrypted_value) + .map_err(|e| { DatabaseOperationError::ApplicationError( - ApplicationError::InvalidInput( - "Failed to extract encrypted content".to_string(), - ), + ApplicationError::InvalidInput(format!( + "Failed to serialize encrypted value: {}", + e + )), ) })?; - if let Some(id) = config.id { + let config_id = if let Some(id) = config.id { // Update existing config tx.execute( "UPDATE provider_configs SET @@ -55,10 +48,11 @@ impl UserProfileDbHandler { config.name, config.provider_type, config.model_identifier, - encrypted_settings, + encrypted_value_json, id as i64 ], )?; + id } else { // Insert new config tx.execute( @@ -70,110 +64,104 @@ impl UserProfileDbHandler { config.name, config.provider_type, config.model_identifier, - encrypted_settings, + encrypted_value_json, encryption_key_id ], )?; - } - - Ok(()) + tx.last_insert_rowid() as usize + }; + + Ok(ProviderConfig { + id: Some(config_id), + name: config.name.clone(), + provider_type: config.provider_type.clone(), + model_identifier: config.model_identifier.clone(), + additional_settings: config.additional_settings.clone(), + }) }) .map_err(ApplicationError::from) } -} -impl UserProfileDbHandler { pub async fn load_provider_configs( &self, ) -> Result, ApplicationError> { let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { let mut stmt = tx.prepare( - "SELECT pc.id, pc.name, pc.provider_type, \ - pc.model_identifier, pc.additional_settings, - ek.file_path as encryption_key_path + "SELECT pc.id, pc.name, pc.provider_type, + pc.model_identifier, pc.additional_settings, + ek.file_path as encryption_key_path, ek.sha256_hash FROM provider_configs pc JOIN encryption_keys ek ON pc.encryption_key_id = ek.id", )?; - let configs = stmt.query_map([], |row| { let id: i64 = row.get(0)?; let name: String = row.get(1)?; let provider_type: String = row.get(2)?; let model_identifier: Option = row.get(3)?; - let additional_settings_encrypted: String = row.get(4)?; + let encrypted_value_json: String = row.get(4)?; let encryption_key_path: String = row.get(5)?; - + let sha256_hash: String = row.get(6)?; Ok(( id, name, provider_type, model_identifier, - additional_settings_encrypted, + encrypted_value_json, encryption_key_path, + sha256_hash, )) })?; let mut result = Vec::new(); - for config in configs { let ( id, name, provider_type, model_identifier, - additional_settings_encrypted, + encrypted_value_json, encryption_key_path, + sha256_hash, ) = config?; - // Load the specific encryption handler for this config - let encryption_handler = EncryptionHandler::new_from_path( - &PathBuf::from(encryption_key_path), - ) - .map_err(|e| { - DatabaseOperationError::ApplicationError( - ApplicationError::EncryptionError( - EncryptionError::KeyGenerationFailed(e.to_string()), - ), - ) - })? - .ok_or_else(|| { - DatabaseOperationError::ApplicationError( - ApplicationError::EncryptionError( - EncryptionError::InvalidKey( - "Failed to create encryption handler" - .to_string(), - ), - ), - ) - })?; - - // Decrypt the additional settings using the specific encryption handler - let decrypted_value = encryption_handler - .decrypt_string( - &additional_settings_encrypted, - "", // The actual key should be retrieved from the encryption handler - ) - .map_err(|e| { - DatabaseOperationError::ApplicationError( - ApplicationError::EncryptionError( - EncryptionError::DecryptionFailed( - e.to_string(), - ), - ), - ) - })?; - - let additional_settings: HashMap = - serde_json::from_str(&decrypted_value).map_err(|e| { - DatabaseOperationError::ApplicationError( - ApplicationError::InvalidInput(format!( - "Failed to deserialize decrypted settings: {}", - e - )), - ) - })?; + // Create a new EncryptionHandler for this config + let encryption_handler = EncryptionHandler::new_from_path(&PathBuf::from(&encryption_key_path)) + .map_err(|e| DatabaseOperationError::ApplicationError(e))? + .ok_or_else(|| DatabaseOperationError::ApplicationError(ApplicationError::EncryptionError( + EncryptionError::InvalidKey("Failed to create encryption handler".to_string()) + )))?; + + // Verify the encryption key hash + if encryption_handler.get_sha256_hash() != sha256_hash { + return Err(DatabaseOperationError::ApplicationError(ApplicationError::EncryptionError( + EncryptionError::InvalidKey("Encryption key hash mismatch".to_string()) + ))); + } + + let encrypted_value: JsonValue = serde_json::from_str(&encrypted_value_json) + .map_err(|e| DatabaseOperationError::ApplicationError(ApplicationError::InvalidInput( + format!("Failed to deserialize encrypted value: {}", e) + )))?; + + // Use the encryption handler to decrypt the value + let decrypted_value = if let (Some(content), Some(encryption_key)) = ( + encrypted_value["content"].as_str(), + encrypted_value["encryption_key"].as_str() + ) { + encryption_handler.decrypt_string(content, encryption_key) + .map_err(|e| DatabaseOperationError::ApplicationError(e))? + } else { + return Err(DatabaseOperationError::ApplicationError( + ApplicationError::InvalidInput("Invalid encrypted value format".to_string()) + )); + }; + + let additional_settings: HashMap = + serde_json::from_str(&decrypted_value) + .map_err(|e| DatabaseOperationError::ApplicationError(ApplicationError::InvalidInput( + format!("Failed to deserialize decrypted settings: {}", e) + )))?; result.push(ProviderConfig { id: Some(id as usize), @@ -183,9 +171,33 @@ impl UserProfileDbHandler { additional_settings, }); } - Ok(result) }) .map_err(ApplicationError::from) } + + pub async fn delete_provider_config( + &self, + config_id: usize, + ) -> Result<(), ApplicationError> { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + let deleted_rows = tx.execute( + "DELETE FROM provider_configs WHERE id = ?", + params![config_id as i64], + )?; + + if deleted_rows == 0 { + Err(DatabaseOperationError::ApplicationError( + ApplicationError::InvalidInput(format!( + "No provider config found with ID {}", + config_id + )), + )) + } else { + Ok(()) + } + }) + .map_err(ApplicationError::from) + } } 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 161ca53..4228e5c 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/mod.rs @@ -8,7 +8,7 @@ mod session; pub use completion_options::ChatCompletionOptions; pub use conversation::{ConversationCache, NewConversation, PromptInstruction}; use prompt::Prompt; -pub use prompt::{AssistantManager, PromptInstructionBuilder, PromptRole}; +pub use prompt::{PromptInstructionBuilder, PromptRole}; pub use session::{prompt_app, App, ChatEvent, ThreadedChatSession}; pub use super::defaults::*; @@ -17,10 +17,10 @@ use super::server::{ CompletionResponse, ModelBackend, ModelServer, ServerManager, }; use super::tui::{ - draw_ui, AdditionalSetting, AppUi, ColorScheme, ColorSchemeType, - CommandLineAction, ConversationEvent, KeyEventHandler, ModalAction, - ModalWindowType, PromptAction, ProviderConfig, SimpleString, TextLine, - TextSegment, TextWindowTrait, UserEvent, WindowEvent, WindowKind, + draw_ui, AppUi, ColorScheme, ColorSchemeType, CommandLineAction, + ConversationEvent, KeyEventHandler, ModalAction, ModalWindowType, + PromptAction, SimpleString, TextLine, TextSegment, TextWindowTrait, + UserEvent, WindowEvent, WindowKind, }; // gets PERSONAS from the generated code 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 6795025..75136c6 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs @@ -14,10 +14,7 @@ pub use events::{ PromptAction, UserEvent, WindowEvent, }; use lumni::api::error::ApplicationError; -pub use modals::{ - AdditionalSetting, ModalAction, ModalWindowTrait, ModalWindowType, - ProviderConfig, -}; +pub use modals::{ModalAction, ModalWindowTrait, ModalWindowType}; pub use ui::AppUi; pub use window::{ CommandLine, ResponseWindow, SimpleString, TextArea, TextLine, TextSegment, @@ -26,8 +23,8 @@ pub use window::{ use super::chat::db::{ Conversation, ConversationDatabase, ConversationDbHandler, ConversationId, - ConversationStatus, MaskMode, ModelIdentifier, ModelSpec, UserProfile, - UserProfileDbHandler, + ConversationStatus, MaskMode, ModelIdentifier, ModelSpec, ProviderConfig, + ProviderConfigOptions, UserProfile, UserProfileDbHandler, }; use super::chat::{ App, NewConversation, PromptInstruction, ThreadedChatSession, 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 84821f5..323d3c7 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,22 +1,22 @@ mod conversations; mod filebrowser; -mod profiles; +mod settings; use async_trait::async_trait; pub use conversations::ConversationListModal; pub use filebrowser::FileBrowserModal; -pub use profiles::{AdditionalSetting, ProfileEditModal, ProviderConfig}; use ratatui::layout::Rect; use ratatui::Frame; +pub use settings::SettingsModal; use widgets::FileBrowserWidget; pub use super::widgets; use super::{ ApplicationError, Conversation, ConversationDbHandler, ConversationStatus, - KeyTrack, MaskMode, ModelIdentifier, ModelServer, ModelSpec, - PromptInstruction, ServerTrait, SimpleString, TextArea, TextWindowTrait, - ThreadedChatSession, UserEvent, UserProfile, UserProfileDbHandler, - WindowEvent, SUPPORTED_MODEL_ENDPOINTS, + KeyTrack, MaskMode, ModelServer, ModelSpec, PromptInstruction, + ProviderConfig, ProviderConfigOptions, ServerTrait, SimpleString, TextArea, + TextWindowTrait, ThreadedChatSession, UserEvent, UserProfile, + UserProfileDbHandler, WindowEvent, SUPPORTED_MODEL_ENDPOINTS, }; #[derive(Debug, Clone, PartialEq)] 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 deleted file mode 100644 index 5d19bd6..0000000 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod profile_edit_modal; -mod profile_edit_renderer; -mod profile_list; -mod provider_manager; -mod settings_editor; - -pub use profile_edit_modal::ProfileEditModal; -pub use provider_manager::{ - AdditionalSetting, ProviderConfig, ProviderManager, -}; - -use super::{ - ApplicationError, ConversationDbHandler, KeyTrack, MaskMode, ModalAction, - ModalWindowTrait, ModalWindowType, ModelIdentifier, ModelServer, ModelSpec, - ServerTrait, SimpleString, ThreadedChatSession, UserProfile, - UserProfileDbHandler, WindowEvent, SUPPORTED_MODEL_ENDPOINTS, -}; - -#[derive(Debug)] -pub enum BackgroundTaskResult { - ProfileCreated(Result), -} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_edit_modal.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_edit_modal.rs deleted file mode 100644 index b828d89..0000000 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_edit_modal.rs +++ /dev/null @@ -1,624 +0,0 @@ -use std::time::Instant; - -use async_trait::async_trait; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::layout::{Alignment, Rect}; -use ratatui::style::{Color, Style}; -use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph}; -use ratatui::Frame; -use serde_json::{json, Value as JsonValue}; -use tokio::sync::mpsc; - -use super::profile_edit_renderer::ProfileEditRenderer; -use super::profile_list::ProfileList; -use super::provider_manager::ProviderManagerAction; -use super::settings_editor::{SettingsAction, SettingsEditor}; -use super::*; - -#[derive(Debug, Clone, PartialEq)] -pub enum ProfileCreationStep { - EnterName, - SelectProvider, - ConfirmCreate, - CreatingProfile, -} - -#[derive(Debug, Clone)] -pub enum ProfileCreatorAction { - Refresh, - WaitForKeyEvent, - Cancel, - CreateProfile, -} - -pub struct ProfileCreator { - pub new_profile_name: String, - pub creation_step: ProfileCreationStep, - pub provider_manager: ProviderManager, - pub db_handler: UserProfileDbHandler, - pub background_task: Option>, - pub task_start_time: Option, - selected_provider: Option, -} - -impl ProfileCreator { - pub fn new(db_handler: UserProfileDbHandler) -> Self { - Self { - new_profile_name: String::new(), - creation_step: ProfileCreationStep::EnterName, - provider_manager: ProviderManager::new(db_handler.clone()), - db_handler, - background_task: None, - task_start_time: None, - selected_provider: None, - } - } - - pub async fn handle_input( - &mut self, - input: KeyEvent, - ) -> Result { - match self.creation_step { - ProfileCreationStep::EnterName => { - self.handle_enter_name(input).await - } - ProfileCreationStep::SelectProvider => { - self.handle_select_provider(input).await - } - ProfileCreationStep::ConfirmCreate => { - self.handle_confirm_create(input) - } - ProfileCreationStep::CreatingProfile => { - Ok(ProfileCreatorAction::WaitForKeyEvent) - } - } - } - - async fn handle_enter_name( - &mut self, - input: KeyEvent, - ) -> Result { - match input.code { - KeyCode::Char(c) => { - self.new_profile_name.push(c); - Ok(ProfileCreatorAction::Refresh) - } - KeyCode::Backspace => { - self.new_profile_name.pop(); - Ok(ProfileCreatorAction::Refresh) - } - KeyCode::Enter => { - if !self.new_profile_name.is_empty() { - self.creation_step = ProfileCreationStep::SelectProvider; - self.provider_manager.load_configs().await?; - Ok(ProfileCreatorAction::Refresh) - } else { - Ok(ProfileCreatorAction::WaitForKeyEvent) - } - } - KeyCode::Esc => Ok(ProfileCreatorAction::Cancel), - _ => Ok(ProfileCreatorAction::WaitForKeyEvent), - } - } - - async fn handle_select_provider( - &mut self, - input: KeyEvent, - ) -> Result { - match self.provider_manager.handle_input(input).await? { - ProviderManagerAction::Refresh => Ok(ProfileCreatorAction::Refresh), - ProviderManagerAction::ProviderSelected => { - self.selected_provider = - self.provider_manager.get_selected_provider().cloned(); - self.creation_step = ProfileCreationStep::ConfirmCreate; - Ok(ProfileCreatorAction::Refresh) - } - ProviderManagerAction::NoAction => { - Ok(ProfileCreatorAction::WaitForKeyEvent) - } - } - } - - fn handle_confirm_create( - &mut self, - input: KeyEvent, - ) -> Result { - match input.code { - KeyCode::Enter => { - self.creation_step = ProfileCreationStep::CreatingProfile; - Ok(ProfileCreatorAction::CreateProfile) - } - KeyCode::Esc => { - self.creation_step = ProfileCreationStep::SelectProvider; - Ok(ProfileCreatorAction::Refresh) - } - _ => Ok(ProfileCreatorAction::WaitForKeyEvent), - } - } - - pub async fn create_profile( - &mut self, - ) -> Result { - let selected_config = self.selected_provider.as_ref().ok_or( - ApplicationError::NotReady( - "No provider config selected".to_string(), - ), - )?; - - let mut settings = serde_json::Map::new(); - settings.insert( - "__TEMPLATE.__MODEL_SERVER".to_string(), - json!(selected_config.provider_type), - ); - if let Some(model) = &selected_config.model_identifier { - settings.insert( - "__TEMPLATE.MODEL_IDENTIFIER".to_string(), - json!(model), - ); - } - for (key, setting) in &selected_config.additional_settings { - let value = if setting.is_secure { - json!({ - "content": setting.value, - "encryption_key": "", - "type_info": "string", - }) - } else { - json!(setting.value) - }; - settings.insert(format!("__TEMPLATE.{}", key), value); - } - - let new_profile = self - .db_handler - .create(&self.new_profile_name, &json!(settings)) - .await?; - Ok(new_profile) - } - - pub fn render(&self, f: &mut Frame, area: Rect) { - match self.creation_step { - ProfileCreationStep::EnterName => self.render_enter_name(f, area), - ProfileCreationStep::SelectProvider => { - self.provider_manager.render(f, area) - } - ProfileCreationStep::ConfirmCreate => { - self.render_confirm_create(f, area) - } - ProfileCreationStep::CreatingProfile => { - self.render_creating_profile(f, area) - } - } - } - - fn render_enter_name(&self, f: &mut Frame, area: Rect) { - let input = Paragraph::new(self.new_profile_name.as_str()) - .style(Style::default().fg(Color::Yellow)) - .block( - Block::default() - .borders(Borders::ALL) - .title("Enter New Profile Name"), - ); - f.render_widget(input, area); - } - - fn render_confirm_create(&self, f: &mut Frame, area: Rect) { - let mut items = - vec![ListItem::new(format!("Name: {}", self.new_profile_name))]; - - if let Some(config) = &self.selected_provider { - items.push(ListItem::new(format!( - "Provider: {}", - config.provider_type - ))); - if let Some(model) = &config.model_identifier { - items.push(ListItem::new(format!("Model: {}", model))); - } - for (key, setting) in &config.additional_settings { - items - .push(ListItem::new(format!("{}: {}", key, setting.value))); - } - } - - let confirm_list = List::new(items).block( - Block::default() - .borders(Borders::ALL) - .title("Confirm Profile Creation"), - ); - f.render_widget(confirm_list, area); - } - - fn render_creating_profile(&self, f: &mut Frame, area: Rect) { - let content = - format!("Creating profile '{}'...", self.new_profile_name); - let paragraph = Paragraph::new(content) - .style(Style::default().fg(Color::Green)) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .title("Creating Profile"), - ); - f.render_widget(paragraph, area); - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Focus { - ProfileList, - SettingsList, - ProfileCreation, - RenamingProfile, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum EditMode { - NotEditing, - EditingValue, - AddingNewKey, - AddingNewValue, - RenamingProfile, -} - -#[derive(Debug)] -pub struct UIState { - pub focus: Focus, - pub edit_mode: EditMode, - pub show_secure: bool, -} - -impl UIState { - pub fn new() -> Self { - UIState { - focus: Focus::ProfileList, - edit_mode: EditMode::NotEditing, - show_secure: false, - } - } - - pub fn set_focus(&mut self, focus: Focus) { - self.focus = focus; - } - - pub fn set_edit_mode(&mut self, mode: EditMode) { - self.edit_mode = mode; - } -} - -pub struct ProfileEditModal { - pub profile_list: ProfileList, - pub settings_editor: SettingsEditor, - pub ui_state: UIState, - db_handler: UserProfileDbHandler, - renderer: ProfileEditRenderer, - pub profile_creator: Option, - new_profile_name: Option, -} - -impl ProfileEditModal { - pub async fn new( - mut db_handler: UserProfileDbHandler, - ) -> Result { - let profiles = db_handler.list_profiles().await?; - let default_profile = db_handler.get_default_profile().await?; - let profile_list = ProfileList::new(profiles, default_profile); - - let settings = - if let Some(profile) = profile_list.get_selected_profile() { - db_handler - .get_profile_settings(profile, MaskMode::Mask) - .await? - } else { - JsonValue::Object(serde_json::Map::new()) - }; - let settings_editor = SettingsEditor::new(settings); - - Ok(Self { - profile_list, - settings_editor, - ui_state: UIState::new(), - db_handler, - renderer: ProfileEditRenderer::new(), - profile_creator: None, - new_profile_name: None, - }) - } - - async fn set_default_profile(&mut self) -> Result<(), ApplicationError> { - let selected_profile = - self.profile_list.get_selected_profile().cloned(); - if let Some(profile) = selected_profile { - self.db_handler.set_default_profile(&profile).await?; - self.profile_list.mark_as_default(&profile); - } - Ok(()) - } - - async fn rename_profile( - &mut self, - new_name: String, - ) -> Result<(), ApplicationError> { - if let Some(profile) = self.profile_list.get_selected_profile() { - self.db_handler.rename_profile(profile, &new_name).await?; - self.profile_list - .rename_profile(new_name, &mut self.db_handler) - .await?; - } - self.ui_state.set_edit_mode(EditMode::NotEditing); - Ok(()) - } - - fn cancel_edit(&mut self) { - self.settings_editor.cancel_edit(); - self.ui_state.set_edit_mode(EditMode::NotEditing); - } - - async fn load_selected_profile_settings( - &mut self, - ) -> Result<(), ApplicationError> { - if let Some(profile) = self.profile_list.get_selected_profile() { - self.settings_editor - .load_settings(profile, &mut self.db_handler) - .await?; - } - Ok(()) - } -} - -#[async_trait] -impl ModalWindowTrait for ProfileEditModal { - fn get_type(&self) -> ModalWindowType { - ModalWindowType::ProfileEdit - } - - fn render_on_frame(&mut self, frame: &mut Frame, area: Rect) { - frame.render_widget(Clear, area); - match self.ui_state.focus { - Focus::ProfileCreation => { - if let Some(creator) = &self.profile_creator { - creator.render(frame, area); - } - } - _ => self.renderer.render_layout(frame, area, self), - } - } - - async fn refresh(&mut self) -> Result { - if let Some(creator) = &mut self.profile_creator { - if let Some(ref mut rx) = creator.background_task { - match rx.try_recv() { - Ok(BackgroundTaskResult::ProfileCreated(result)) => { - creator.background_task = None; - creator.task_start_time = None; - match result { - Ok(new_profile) => { - self.profile_list.add_profile(new_profile); - self.profile_creator = None; - self.ui_state.focus = Focus::ProfileList; - } - Err(e) => { - log::error!("Failed to create profile: {}", e); - } - } - return Ok(WindowEvent::Modal(ModalAction::Refresh)); - } - Err(mpsc::error::TryRecvError::Empty) => { - return Ok(WindowEvent::Modal(ModalAction::Refresh)); - } - Err(mpsc::error::TryRecvError::Disconnected) => { - self.profile_creator = None; - self.ui_state.focus = Focus::ProfileList; - return Ok(WindowEvent::Modal(ModalAction::Refresh)); - } - } - } - } - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - - async fn handle_key_event<'b>( - &'b mut self, - key_event: &'b mut KeyTrack, - _tab_chat: &'b mut ThreadedChatSession, - _handler: &mut ConversationDbHandler, - ) -> Result { - let key_code = key_event.current_key().code; - - match self.ui_state.focus { - Focus::ProfileCreation => { - if let Some(creator) = &mut self.profile_creator { - let action = creator - .handle_input(key_event.current_key().clone()) - .await?; - match action { - ProfileCreatorAction::Refresh => { - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - ProfileCreatorAction::WaitForKeyEvent => { - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - ProfileCreatorAction::Cancel => { - self.profile_creator = None; - self.ui_state.focus = Focus::ProfileList; - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - ProfileCreatorAction::CreateProfile => { - let new_profile = creator.create_profile().await?; - self.profile_list.add_profile(new_profile); - self.profile_creator = None; - self.ui_state.focus = Focus::ProfileList; - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - } - } else { - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - } - Focus::ProfileList => { - match key_code { - KeyCode::Up => { - self.profile_list.move_selection_up(); - self.load_selected_profile_settings().await?; - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - KeyCode::Down => { - self.profile_list.move_selection_down(); - self.load_selected_profile_settings().await?; - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - KeyCode::Enter => { - if self.profile_list.is_new_profile_selected() { - // Start new profile creation - self.profile_creator = Some(ProfileCreator::new( - self.db_handler.clone(), - )); - self.ui_state.focus = Focus::ProfileCreation; - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } else { - // Handle selecting an existing profile if needed - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - } - KeyCode::Char('n') => { - self.profile_creator = - Some(ProfileCreator::new(self.db_handler.clone())); - self.ui_state.focus = Focus::ProfileCreation; - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - KeyCode::Char('r') | KeyCode::Char('R') => { - if let Some(profile) = - self.profile_list.get_selected_profile() - { - self.ui_state - .set_edit_mode(EditMode::RenamingProfile); - self.new_profile_name = Some(profile.name.clone()); - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } else { - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - } - KeyCode::Char(' ') => { - self.set_default_profile().await?; - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - KeyCode::Char('D') => { - self.profile_list - .delete_profile(&mut self.db_handler) - .await?; - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - KeyCode::Tab => { - self.ui_state.set_focus(Focus::SettingsList); - if let Some(profile) = - self.profile_list.get_selected_profile() - { - self.settings_editor - .load_settings(profile, &mut self.db_handler) - .await?; - } - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - KeyCode::Esc => Ok(WindowEvent::PromptWindow(None)), - _ => Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)), - } - } - Focus::RenamingProfile => match key_code { - KeyCode::Enter => { - if let Some(new_name) = self.new_profile_name.take() { - self.rename_profile(new_name).await?; - } - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - KeyCode::Char(c) => { - if let Some(ref mut name) = self.new_profile_name { - name.push(c); - } - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - KeyCode::Backspace => { - if let Some(ref mut name) = self.new_profile_name { - name.pop(); - } - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - KeyCode::Esc => { - self.new_profile_name = None; - self.ui_state.set_edit_mode(EditMode::NotEditing); - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - _ => Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)), - }, - Focus::SettingsList => { - let (new_mode, handled, action) = self - .settings_editor - .handle_key_event(key_code, self.ui_state.edit_mode); - - if handled { - if let Some(action) = action { - if let Some(profile) = - self.profile_list.get_selected_profile() - { - match action { - SettingsAction::ToggleSecureVisibility => { - self.settings_editor - .toggle_secure_visibility( - profile, - &mut self.db_handler, - ) - .await?; - } - SettingsAction::DeleteCurrentKey => { - self.settings_editor - .delete_current_key( - profile, - &mut self.db_handler, - ) - .await?; - } - SettingsAction::ClearCurrentKey => { - self.settings_editor - .clear_current_key( - profile, - &mut self.db_handler, - ) - .await?; - } - SettingsAction::SaveEdit => { - self.settings_editor - .save_edit( - profile, - &mut self.db_handler, - ) - .await?; - } - SettingsAction::SaveNewValue => { - self.settings_editor - .save_new_value( - profile, - &mut self.db_handler, - ) - .await?; - } - } - } - } - self.ui_state.set_edit_mode(new_mode); - return Ok(WindowEvent::Modal(ModalAction::Refresh)); - } - - if self.ui_state.edit_mode == EditMode::NotEditing - && (key_code == KeyCode::Left - || key_code == KeyCode::Char('q') - || key_code == KeyCode::Esc - || key_code == KeyCode::Tab) - { - self.ui_state.set_focus(Focus::ProfileList); - return Ok(WindowEvent::Modal(ModalAction::Refresh)); - } - - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - _ => Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)), - } - } -} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_edit_renderer.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_edit_renderer.rs deleted file mode 100644 index 2cc5e4f..0000000 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_edit_renderer.rs +++ /dev/null @@ -1,252 +0,0 @@ -use ratatui::prelude::*; -use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; - -use super::profile_edit_modal::{EditMode, Focus, ProfileEditModal}; - -pub struct ProfileEditRenderer; - -impl ProfileEditRenderer { - pub fn new() -> Self { - ProfileEditRenderer - } - - pub fn render_title(&self, f: &mut Frame, area: Rect) { - let title = Paragraph::new("Profile Editor") - .style(Style::default().fg(Color::Cyan)) - .alignment(Alignment::Center); - f.render_widget(title, area); - } - - pub fn render_profile_list( - &self, - f: &mut Frame, - area: Rect, - profile_edit_modal: &ProfileEditModal, - ) { - let profiles = profile_edit_modal.profile_list.get_profiles(); - let mut items: Vec = profiles - .iter() - .enumerate() - .map(|(i, profile)| { - let content = profile; - let style = if i - == profile_edit_modal.profile_list.get_selected_index() - && matches!( - profile_edit_modal.ui_state.focus, - Focus::ProfileList | Focus::RenamingProfile - ) { - 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(content, style)])) - }) - .collect(); - - // Add "New Profile" option - let new_profile_style = if profile_edit_modal - .profile_list - .is_new_profile_selected() - && matches!(profile_edit_modal.ui_state.focus, Focus::ProfileList) - { - Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White) - } else { - Style::default().bg(Color::Black).fg(Color::Green) - }; - items.push(ListItem::new(Line::from(vec![Span::styled( - "+ New Profile", - new_profile_style, - )]))); - - 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(profile_edit_modal.profile_list.get_selected_index())); - - f.render_stateful_widget(list, area, &mut state); - } - - pub fn render_settings_list( - &self, - f: &mut Frame, - area: Rect, - profile_edit_modal: &ProfileEditModal, - ) { - let settings = profile_edit_modal.settings_editor.get_settings(); - let mut items: Vec = settings - .as_object() - .unwrap() - .iter() - .enumerate() - .map(|(i, (key, value))| { - let is_editable = !key.starts_with("__"); - let display_value = - profile_edit_modal.settings_editor.get_display_value(value); - - let content = if matches!( - profile_edit_modal.ui_state.edit_mode, - EditMode::EditingValue - ) && i - == profile_edit_modal.settings_editor.get_current_field() - && is_editable - { - format!( - "{}: {}", - key, - profile_edit_modal.settings_editor.get_edit_buffer() - ) - } else { - format!("{}: {}", key, display_value) - }; - - let style = if i - == profile_edit_modal.settings_editor.get_current_field() - && matches!( - profile_edit_modal.ui_state.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().bg(Color::Black).fg(Color::DarkGray) - }; - ListItem::new(Line::from(vec![Span::styled(content, style)])) - }) - .collect(); - - // Add new key input field if in AddingNewKey mode - if matches!( - profile_edit_modal.ui_state.edit_mode, - EditMode::AddingNewKey - ) { - let secure_indicator = - if profile_edit_modal.settings_editor.is_new_value_secure() { - "🔒 " - } else { - "" - }; - items.push(ListItem::new(Line::from(vec![Span::styled( - format!( - "{}New key: {}", - secure_indicator, - profile_edit_modal.settings_editor.get_new_key_buffer() - ), - Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White), - )]))); - } - - // Add new value input field if in AddingNewValue mode - if matches!( - profile_edit_modal.ui_state.edit_mode, - EditMode::AddingNewValue - ) { - let secure_indicator = - if profile_edit_modal.settings_editor.is_new_value_secure() { - "🔒 " - } else { - "" - }; - items.push(ListItem::new(Line::from(vec![Span::styled( - format!( - "{}{}: {}", - secure_indicator, - profile_edit_modal.settings_editor.get_new_key_buffer(), - profile_edit_modal.settings_editor.get_edit_buffer() - ), - Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White), - )]))); - } - - let list = List::new(items) - .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( - profile_edit_modal.settings_editor.get_current_field(), - )); - - f.render_stateful_widget(list, area, &mut state); - } - - pub fn render_instructions( - &self, - f: &mut Frame, - area: Rect, - profile_edit_modal: &ProfileEditModal, - ) { - let instructions = match ( - &profile_edit_modal.ui_state.focus, - &profile_edit_modal.ui_state.edit_mode, - ) { - (Focus::ProfileList, EditMode::NotEditing) => { - "↑↓: Navigate | Enter: Select/Create | R: Rename | D: Delete | \ - Space: Set Default | →/Tab: Settings | Esc: Close" - } - (Focus::RenamingProfile, EditMode::RenamingProfile) => { - "Enter: Confirm Rename | Esc: Cancel" - } - (Focus::SettingsList, EditMode::NotEditing) => { - "↑↓: Navigate | Enter: Edit | n: New | N: New Secure | D: \ - Delete | C: Clear | S: Show/Hide Secure | ←/Tab/q/Esc: \ - Profiles" - } - (Focus::SettingsList, EditMode::EditingValue) => { - "Enter: Save | Esc: Cancel" - } - (Focus::SettingsList, EditMode::AddingNewKey) => { - "Enter: Confirm Key | Esc: Cancel" - } - (Focus::SettingsList, EditMode::AddingNewValue) => { - "Enter: Save New Value | Esc: Cancel" - } - (Focus::ProfileCreation, _) => { - "Follow on-screen instructions to create a new profile" - } - _ => "", - }; - - let paragraph = Paragraph::new(instructions) - .style(Style::default().fg(Color::Cyan)) - .alignment(Alignment::Center) - .block(Block::default().borders(Borders::TOP)); - f.render_widget(paragraph, area); - } - - pub fn render_layout( - &self, - f: &mut Frame, - area: Rect, - profile_edit_modal: &ProfileEditModal, - ) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Title - Constraint::Min(1), // Main content - Constraint::Length(3), // Instructions - ]) - .split(area); - - self.render_title(f, chunks[0]); - - let main_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(30), - Constraint::Percentage(70), - ]) - .split(chunks[1]); - - self.render_profile_list(f, main_chunks[0], profile_edit_modal); - self.render_settings_list(f, main_chunks[1], profile_edit_modal); - - self.render_instructions(f, chunks[2], profile_edit_modal); - } -} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/settings_editor.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/settings_editor.rs deleted file mode 100644 index 5814035..0000000 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/settings_editor.rs +++ /dev/null @@ -1,447 +0,0 @@ -use crossterm::event::KeyCode; -use serde_json::{json, Map, Value as JsonValue}; - -use super::profile_edit_modal::EditMode; -use super::*; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum SettingsAction { - ToggleSecureVisibility, - DeleteCurrentKey, - ClearCurrentKey, - SaveEdit, - SaveNewValue, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct SettingsEditor { - settings: JsonValue, - current_field: usize, - edit_buffer: String, - new_key_buffer: String, - is_new_value_secure: bool, - show_secure: bool, -} - -impl SettingsEditor { - pub fn new(settings: JsonValue) -> Self { - Self { - settings, - current_field: 0, - edit_buffer: String::new(), - new_key_buffer: String::new(), - is_new_value_secure: false, - show_secure: false, - } - } - - pub fn clear(&mut self) { - self.settings = JsonValue::Object(serde_json::Map::new()); - self.current_field = 0; - self.edit_buffer.clear(); - self.new_key_buffer.clear(); - self.is_new_value_secure = false; - self.show_secure = false; - } - - pub fn get_settings(&self) -> &JsonValue { - &self.settings - } - - pub fn move_selection_up(&mut self) { - if self.current_field > 0 { - self.current_field -= 1; - } - } - - pub fn move_selection_down(&mut self) { - let settings_len = self.settings.as_object().map_or(0, |obj| obj.len()); - if settings_len > 0 && self.current_field < settings_len - 1 { - self.current_field += 1; - } - } - - pub fn start_editing(&mut self) -> Option { - let current_key = self - .settings - .as_object() - .unwrap() - .keys() - .nth(self.current_field) - .unwrap(); - if !current_key.starts_with("__") { - let value = &self.settings[current_key]; - self.edit_buffer = match value { - JsonValue::Object(obj) - if obj.get("was_encrypted") - == Some(&JsonValue::Bool(true)) => - { - match obj.get("value") { - Some(JsonValue::Number(n)) => n.to_string(), - Some(JsonValue::String(s)) => s.clone(), - _ => "".to_string(), - } - } - JsonValue::Number(n) => n.to_string(), - JsonValue::String(s) => s.clone(), - _ => value.to_string(), - }; - Some(self.edit_buffer.clone()) - } else { - None - } - } - - pub fn start_adding_new_value(&mut self, is_secure: bool) { - self.new_key_buffer.clear(); - self.edit_buffer.clear(); - self.is_new_value_secure = is_secure; - } - - pub fn confirm_new_key(&mut self) -> bool { - !self.new_key_buffer.is_empty() - } - - pub async fn save_edit( - &mut self, - profile: &UserProfile, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError> { - let current_key = self - .settings - .as_object() - .unwrap() - .keys() - .nth(self.current_field) - .unwrap() - .to_string(); - - let current_value = &self.settings[¤t_key]; - let is_encrypted = if let Some(obj) = current_value.as_object() { - obj.contains_key("was_encrypted") - && obj["was_encrypted"].as_bool().unwrap_or(false) - } else { - false - }; - - let new_value = if is_encrypted { - json!({ - "content": self.edit_buffer, - "encryption_key": "", // signal that the value must be encrypted - "type_info": "string", - }) - } else { - serde_json::Value::String(self.edit_buffer.clone()) - }; - - let mut update_settings = JsonValue::Object(serde_json::Map::new()); - update_settings[¤t_key] = new_value; - db_handler.update(profile, &update_settings).await?; - - // Reload settings to reflect the changes - self.load_settings(profile, db_handler).await?; - - Ok(()) - } - - pub fn get_display_value(&self, value: &JsonValue) -> String { - match value { - JsonValue::Object(obj) - if obj.get("was_encrypted") == Some(&JsonValue::Bool(true)) => - { - let display = if self.show_secure { - match obj.get("content") { - Some(JsonValue::String(s)) => s.clone(), - _ => "Invalid Value".to_string(), - } - } else { - "*****".to_string() - }; - format!("{} (Encrypted)", display) - } - JsonValue::String(s) => s.clone(), - JsonValue::Number(n) => n.to_string(), - JsonValue::Bool(b) => b.to_string(), - JsonValue::Null => "null".to_string(), - _ => value.to_string(), - } - } - - pub async fn save_new_value( - &mut self, - profile: &UserProfile, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError> { - let new_key = self.new_key_buffer.clone(); - let new_value = if self.is_new_value_secure { - json!({ - "content": self.edit_buffer.clone(), - "encryption_key": "", // signal that value must be encrypted, encryption key will be set by the handler - "type_info": "string", - }) - } else { - serde_json::Value::String(self.edit_buffer.clone()) - }; - - let mut update_settings = JsonValue::Object(serde_json::Map::new()); - update_settings[&new_key] = new_value.clone(); - db_handler.update(profile, &update_settings).await?; - - // Update the local settings for feedback to the user - if let Some(obj) = self.settings.as_object_mut() { - if self.is_new_value_secure { - obj.insert( - new_key.clone(), - json!({ - "content": self.edit_buffer.clone(), - "was_encrypted": true - }), - ); - } else { - obj.insert(new_key.clone(), new_value); - } - } - - // Set the current field to the newly added key - self.current_field = self.find_key_index(&new_key); - - // Reset buffers and flags - self.edit_buffer.clear(); - self.new_key_buffer.clear(); - self.is_new_value_secure = false; - - Ok(()) - } - - fn find_key_index(&self, key: &str) -> usize { - self.settings - .as_object() - .map(|obj| obj.keys().position(|k| k == key).unwrap_or(0)) - .unwrap_or(0) - } - - pub fn cancel_edit(&mut self) { - self.edit_buffer.clear(); - self.new_key_buffer.clear(); - self.is_new_value_secure = false; - } - - pub async fn delete_current_key( - &mut self, - profile: &UserProfile, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError> { - if let Some(current_key) = self - .settings - .as_object() - .unwrap() - .keys() - .nth(self.current_field) - { - let current_key = current_key.to_string(); - if !current_key.starts_with("__") { - let mut settings = Map::new(); - settings.insert(current_key, JsonValue::Null); // Null indicates deletion - db_handler - .update(profile, &JsonValue::Object(settings)) - .await?; - self.load_settings(profile, db_handler).await?; - } - } - Ok(()) - } - - pub async fn clear_current_key( - &mut self, - profile: &UserProfile, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError> { - if let Some(current_key) = self - .settings - .as_object() - .unwrap() - .keys() - .nth(self.current_field) - { - let current_key = current_key.to_string(); - if !current_key.starts_with("__") { - self.settings[¤t_key] = JsonValue::String("".to_string()); - db_handler.update(profile, &self.settings).await?; - } - } - Ok(()) - } - - pub async fn toggle_secure_visibility( - &mut self, - profile: &UserProfile, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError> { - // Store the current key before toggling - let current_key = self.get_current_key().map(String::from); - - self.show_secure = !self.show_secure; - self.load_settings(profile, db_handler).await?; - - // Restore the selection after reloading settings - if let Some(key) = current_key { - self.current_field = self.find_key_index(&key); - } - - Ok(()) - } - - fn get_current_key(&self) -> Option<&str> { - self.settings - .as_object() - .and_then(|obj| obj.keys().nth(self.current_field)) - .map(String::as_str) - } - - pub async fn load_settings( - &mut self, - profile: &UserProfile, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError> { - let mask_mode = if self.show_secure { - MaskMode::Unmask - } else { - MaskMode::Mask - }; - self.settings = - db_handler.get_profile_settings(profile, mask_mode).await?; - eprintln!("Settings: {:?}", self.settings); - self.current_field = 0; - Ok(()) - } - - pub fn handle_key_event( - &mut self, - key_code: KeyCode, - current_mode: EditMode, - ) -> (EditMode, bool, Option) { - match current_mode { - EditMode::NotEditing => match key_code { - KeyCode::Up => { - self.move_selection_up(); - (EditMode::NotEditing, true, None) - } - KeyCode::Down => { - self.move_selection_down(); - (EditMode::NotEditing, true, None) - } - KeyCode::Enter => { - if self.start_editing().is_some() { - (EditMode::EditingValue, true, None) - } else { - (EditMode::NotEditing, false, None) - } - } - KeyCode::Char('n') => { - self.start_adding_new_value(false); - (EditMode::AddingNewKey, true, None) - } - KeyCode::Char('N') => { - self.start_adding_new_value(true); - (EditMode::AddingNewKey, true, None) - } - KeyCode::Char('s') | KeyCode::Char('S') => ( - EditMode::NotEditing, - true, - Some(SettingsAction::ToggleSecureVisibility), - ), - KeyCode::Char('D') => ( - EditMode::NotEditing, - true, - Some(SettingsAction::DeleteCurrentKey), - ), - KeyCode::Char('C') => ( - EditMode::NotEditing, - true, - Some(SettingsAction::ClearCurrentKey), - ), - _ => (EditMode::NotEditing, false, None), - }, - EditMode::EditingValue => match key_code { - KeyCode::Enter => { - (EditMode::NotEditing, true, Some(SettingsAction::SaveEdit)) - } - KeyCode::Esc => { - self.cancel_edit(); - (EditMode::NotEditing, true, None) - } - KeyCode::Backspace => { - self.edit_buffer.pop(); - (EditMode::EditingValue, true, None) - } - KeyCode::Char(c) => { - self.edit_buffer.push(c); - (EditMode::EditingValue, true, None) - } - _ => (EditMode::EditingValue, false, None), - }, - EditMode::AddingNewKey => match key_code { - KeyCode::Enter => { - if self.confirm_new_key() { - (EditMode::AddingNewValue, true, None) - } else { - (EditMode::AddingNewKey, false, None) - } - } - KeyCode::Esc => { - self.cancel_edit(); - (EditMode::NotEditing, true, None) - } - KeyCode::Backspace => { - self.new_key_buffer.pop(); - (EditMode::AddingNewKey, true, None) - } - KeyCode::Char(c) => { - self.new_key_buffer.push(c); - (EditMode::AddingNewKey, true, None) - } - _ => (EditMode::AddingNewKey, false, None), - }, - EditMode::AddingNewValue => match key_code { - KeyCode::Enter => ( - EditMode::NotEditing, - true, - Some(SettingsAction::SaveNewValue), - ), - KeyCode::Esc => { - self.cancel_edit(); - (EditMode::NotEditing, true, None) - } - KeyCode::Backspace => { - self.edit_buffer.pop(); - (EditMode::AddingNewValue, true, None) - } - KeyCode::Char(c) => { - self.edit_buffer.push(c); - (EditMode::AddingNewValue, true, None) - } - _ => (EditMode::AddingNewValue, false, None), - }, - EditMode::RenamingProfile => { - (EditMode::RenamingProfile, false, None) - } - } - } - - // Getter methods for UI rendering - pub fn get_current_field(&self) -> usize { - self.current_field - } - - pub fn get_edit_buffer(&self) -> &str { - &self.edit_buffer - } - - pub fn get_new_key_buffer(&self) -> &str { - &self.new_key_buffer - } - - pub fn is_new_value_secure(&self) -> bool { - self.is_new_value_secure - } -} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/mod.rs new file mode 100644 index 0000000..7512a37 --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/mod.rs @@ -0,0 +1,200 @@ +mod profile; +mod provider; +mod settings_editor; + +use async_trait::async_trait; +use crossterm::event::{KeyCode, KeyEvent}; +use profile::{ProfileEditRenderer, ProfileManager}; +use provider::ProviderManager; +use ratatui::layout::Rect; +use ratatui::widgets::Clear; +use ratatui::Frame; +pub use settings_editor::{SettingsAction, SettingsEditor}; + +use super::{ + ApplicationError, ConversationDbHandler, KeyTrack, MaskMode, ModalAction, + ModalWindowTrait, ModalWindowType, ModelServer, ModelSpec, ProviderConfig, + ProviderConfigOptions, ServerTrait, SimpleString, ThreadedChatSession, + UserProfile, UserProfileDbHandler, WindowEvent, SUPPORTED_MODEL_ENDPOINTS, +}; + +#[derive(Debug)] +pub enum BackgroundTaskResult { + ProfileCreated(Result), +} +pub trait GenericList { + fn get_items(&self) -> Vec; + fn get_selected_index(&self) -> usize; +} + +pub trait Creator { + fn render(&self, f: &mut Frame, area: Rect); +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum EditMode { + NotEditing, + EditingValue, + AddingNewKey, + AddingNewValue, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TabFocus { + List, + Settings, + Creation, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum EditTab { + Profiles, + Providers, +} +pub struct SettingsModal { + pub current_tab: EditTab, + pub tab_focus: TabFocus, + pub profile_manager: ProfileManager, + pub provider_manager: ProviderManager, + renderer: ProfileEditRenderer, +} + +impl SettingsModal { + pub async fn new( + db_handler: UserProfileDbHandler, + ) -> Result { + let profile_manager = ProfileManager::new(db_handler.clone()).await?; + + Ok(Self { + current_tab: EditTab::Profiles, + tab_focus: TabFocus::List, + profile_manager, + provider_manager: ProviderManager::new(db_handler.clone()).await?, + renderer: ProfileEditRenderer::new(), + }) + } + + pub async fn handle_key_event( + &mut self, + key_event: KeyEvent, + ) -> Result { + match key_event.code { + KeyCode::Tab => { + self.switch_tab().await?; + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Esc => { + if self.tab_focus == TabFocus::Settings { + self.tab_focus = TabFocus::List; + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } else { + Ok(WindowEvent::PromptWindow(None)) + } + } + _ => match self.current_tab { + EditTab::Profiles => { + self.profile_manager + .handle_key_event(key_event, &mut self.tab_focus) + .await + } + EditTab::Providers => { + self.provider_manager + .handle_key_event(key_event, &mut self.tab_focus) + .await + } + }, + } + } + + async fn switch_tab(&mut self) -> Result<(), ApplicationError> { + self.current_tab = match self.current_tab { + EditTab::Profiles => EditTab::Providers, + EditTab::Providers => EditTab::Profiles, + }; + self.tab_focus = TabFocus::List; + self.refresh_list().await?; + Ok(()) + } + + pub fn get_current_list(&self) -> &dyn GenericList { + match self.current_tab { + EditTab::Profiles => &self.profile_manager.list, + EditTab::Providers => &self.provider_manager.list, + } + } + + pub fn get_current_settings_editor(&self) -> &SettingsEditor { + match self.current_tab { + EditTab::Profiles => &self.profile_manager.settings_editor, + EditTab::Providers => &self.provider_manager.settings_editor, + } + } + + pub fn get_current_creator(&self) -> Option<&dyn Creator> { + match self.current_tab { + EditTab::Profiles => self + .profile_manager + .creator + .as_ref() + .map(|c| c as &dyn Creator), + EditTab::Providers => self + .provider_manager + .creator + .as_ref() + .map(|c| c as &dyn Creator), + } + } + + pub async fn refresh_list( + &mut self, + ) -> Result { + match self.current_tab { + EditTab::Profiles => { + self.profile_manager.refresh_profile_list().await + } + EditTab::Providers => { + self.provider_manager.refresh_provider_list().await + } + } + } + + pub fn get_rename_buffer(&self) -> Option<&String> { + match self.current_tab { + EditTab::Profiles => self.profile_manager.get_rename_buffer(), + EditTab::Providers => None, // TODO: Implement + } + } +} + +#[async_trait] +impl ModalWindowTrait for SettingsModal { + fn get_type(&self) -> ModalWindowType { + ModalWindowType::ProfileEdit + } + + fn render_on_frame(&mut self, frame: &mut Frame, area: Rect) { + frame.render_widget(Clear, area); + match self.current_tab { + EditTab::Profiles => self.renderer.render_layout(frame, area, self), + EditTab::Providers => { + self.renderer.render_layout(frame, area, self) + } + // TODO: move render to specific renderer + //EditTab::Providers => self.provider_manager.render(frame, area), + } + } + + async fn refresh(&mut self) -> Result { + // Runs when a list item is being created or updated in the background + self.refresh_list().await + } + + async fn handle_key_event<'b>( + &'b mut self, + key_event: &'b mut KeyTrack, + _tab_chat: &'b mut ThreadedChatSession, + _handler: &mut ConversationDbHandler, + ) -> Result { + self.handle_key_event(key_event.current_key().clone()).await + } +} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/creator.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/creator.rs new file mode 100644 index 0000000..f4c71c6 --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/creator.rs @@ -0,0 +1,378 @@ +use std::time::Instant; + +use ratatui::layout::Alignment; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; +use serde_json::json; +use tokio::sync::mpsc; + +use super::provider::{ProviderCreator, ProviderCreatorAction}; +use super::*; + +#[derive(Debug, Clone, PartialEq)] +pub enum ProfileCreationStep { + EnterName, + SelectProvider, + CreateProvider, + ConfirmCreate, + CreatingProfile, +} + +#[derive(Debug, Clone)] +pub enum ProfileCreatorAction { + Refresh, + WaitForKeyEvent, + Cancel, + CreateProfile, + SwitchToProviderCreation, + FinishProviderCreation(ProviderConfig), +} + +pub struct ProfileCreator { + pub new_profile_name: String, + pub creation_step: ProfileCreationStep, + db_handler: UserProfileDbHandler, + pub background_task: Option>, + pub task_start_time: Option, + selected_provider: Option, + provider_configs: Vec, + selected_provider_index: Option, + provider_creator: Option, +} + +impl ProfileCreator { + pub async fn new( + db_handler: UserProfileDbHandler, + ) -> Result { + let provider_configs = db_handler.load_provider_configs().await?; + + Ok(Self { + new_profile_name: String::new(), + creation_step: ProfileCreationStep::EnterName, + db_handler, + background_task: None, + task_start_time: None, + selected_provider: None, + provider_configs, + selected_provider_index: None, + provider_creator: None, + }) + } + + pub async fn handle_input( + &mut self, + input: KeyEvent, + ) -> Result { + match self.creation_step { + ProfileCreationStep::EnterName => self.handle_enter_name(input), + ProfileCreationStep::SelectProvider => { + self.handle_select_provider(input).await + } + ProfileCreationStep::CreateProvider => { + self.handle_create_provider(input).await + } + ProfileCreationStep::ConfirmCreate => { + self.handle_confirm_create(input) + } + ProfileCreationStep::CreatingProfile => { + Ok(ProfileCreatorAction::WaitForKeyEvent) + } + } + } + + fn handle_enter_name( + &mut self, + input: KeyEvent, + ) -> Result { + match input.code { + KeyCode::Char(c) => { + self.new_profile_name.push(c); + Ok(ProfileCreatorAction::Refresh) + } + KeyCode::Backspace => { + self.new_profile_name.pop(); + Ok(ProfileCreatorAction::Refresh) + } + KeyCode::Enter => { + if !self.new_profile_name.is_empty() { + self.creation_step = ProfileCreationStep::SelectProvider; + Ok(ProfileCreatorAction::Refresh) + } else { + Ok(ProfileCreatorAction::WaitForKeyEvent) + } + } + KeyCode::Esc => Ok(ProfileCreatorAction::Cancel), + _ => Ok(ProfileCreatorAction::WaitForKeyEvent), + } + } + + async fn handle_select_provider( + &mut self, + input: KeyEvent, + ) -> Result { + match input.code { + KeyCode::Up => { + if let Some(index) = self.selected_provider_index.as_mut() { + if *index > 0 { + *index -= 1; + } else { + *index = self.provider_configs.len(); // Wrap to "Create new Provider" option + } + } else { + self.selected_provider_index = + Some(self.provider_configs.len()); // Select "Create new Provider" + } + Ok(ProfileCreatorAction::Refresh) + } + KeyCode::Down => { + if let Some(index) = self.selected_provider_index.as_mut() { + if *index < self.provider_configs.len() { + *index += 1; + } else { + *index = 0; // Wrap to first provider + } + } else { + self.selected_provider_index = Some(0); + } + Ok(ProfileCreatorAction::Refresh) + } + KeyCode::Enter => { + if let Some(index) = self.selected_provider_index { + if index == self.provider_configs.len() { + // "Create new Provider" option selected + self.creation_step = + ProfileCreationStep::CreateProvider; + self.provider_creator = Some( + ProviderCreator::new(self.db_handler.clone()) + .await?, + ); + Ok(ProfileCreatorAction::SwitchToProviderCreation) + } else { + // Existing provider selected + self.selected_provider = + Some(self.provider_configs[index].clone()); + self.creation_step = ProfileCreationStep::ConfirmCreate; + Ok(ProfileCreatorAction::Refresh) + } + } else { + Ok(ProfileCreatorAction::WaitForKeyEvent) + } + } + KeyCode::Esc => { + self.creation_step = ProfileCreationStep::EnterName; + Ok(ProfileCreatorAction::Refresh) + } + _ => Ok(ProfileCreatorAction::WaitForKeyEvent), + } + } + + async fn handle_create_provider( + &mut self, + input: KeyEvent, + ) -> Result { + if let Some(creator) = &mut self.provider_creator { + match creator.handle_input(input).await { + ProviderCreatorAction::Finish(new_config) => { + self.provider_configs.push(new_config.clone()); + self.selected_provider = Some(new_config.clone()); + self.selected_provider_index = + Some(self.provider_configs.len() - 1); + self.creation_step = ProfileCreationStep::SelectProvider; + self.provider_creator = None; + Ok(ProfileCreatorAction::FinishProviderCreation(new_config)) + } + ProviderCreatorAction::Cancel => { + self.creation_step = ProfileCreationStep::SelectProvider; + self.provider_creator = None; + Ok(ProfileCreatorAction::Refresh) + } + ProviderCreatorAction::Refresh => { + Ok(ProfileCreatorAction::Refresh) + } + ProviderCreatorAction::WaitForKeyEvent => { + Ok(ProfileCreatorAction::WaitForKeyEvent) + } + ProviderCreatorAction::LoadModels => { + creator.load_models().await?; + Ok(ProfileCreatorAction::Refresh) + } + ProviderCreatorAction::LoadAdditionalSettings => { + let model_server = + ModelServer::from_str(&creator.provider_type)?; + creator.prepare_additional_settings(&model_server); + Ok(ProfileCreatorAction::Refresh) + } + ProviderCreatorAction::NoAction => { + Ok(ProfileCreatorAction::WaitForKeyEvent) + } + } + } else { + Ok(ProfileCreatorAction::WaitForKeyEvent) + } + } + + fn handle_confirm_create( + &mut self, + input: KeyEvent, + ) -> Result { + match input.code { + KeyCode::Enter => { + self.creation_step = ProfileCreationStep::CreatingProfile; + Ok(ProfileCreatorAction::CreateProfile) + } + KeyCode::Esc => { + self.creation_step = ProfileCreationStep::SelectProvider; + Ok(ProfileCreatorAction::Refresh) + } + _ => Ok(ProfileCreatorAction::WaitForKeyEvent), + } + } + + pub async fn create_profile( + &mut self, + db_handler: &mut UserProfileDbHandler, + ) -> Result { + let mut settings = serde_json::Map::new(); + if let Some(selected_config) = &self.selected_provider { + settings.insert( + "__TEMPLATE.__MODEL_SERVER".to_string(), + json!(selected_config.provider_type), + ); + if let Some(model) = &selected_config.model_identifier { + settings.insert( + "__TEMPLATE.MODEL_IDENTIFIER".to_string(), + json!(model), + ); + } + for (key, setting) in &selected_config.additional_settings { + let value = if setting.is_secure { + json!({ + "content": setting.value, + "encryption_key": "", + "type_info": "string", + }) + } else { + json!(setting.value) + }; + settings.insert(format!("__TEMPLATE.{}", key), value); + } + } + + let new_profile = db_handler + .create(&self.new_profile_name, &json!(settings)) + .await?; + Ok(new_profile) + } + + pub fn render(&self, f: &mut Frame, area: Rect) { + match self.creation_step { + ProfileCreationStep::EnterName => self.render_enter_name(f, area), + ProfileCreationStep::SelectProvider => { + self.render_select_provider(f, area) + } + ProfileCreationStep::CreateProvider => { + if let Some(creator) = &self.provider_creator { + creator.render(f, area); + } + } + ProfileCreationStep::ConfirmCreate => { + self.render_confirm_create(f, area) + } + ProfileCreationStep::CreatingProfile => { + self.render_creating_profile(f, area) + } + } + } + + fn render_enter_name(&self, f: &mut Frame, area: Rect) { + let input = Paragraph::new(self.new_profile_name.as_str()) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title("Enter New Profile Name"), + ); + f.render_widget(input, area); + } + + fn render_select_provider(&self, f: &mut Frame, area: Rect) { + let mut items: Vec = self + .provider_configs + .iter() + .map(|config| { + ListItem::new(format!( + "{}: {}", + config.name, config.provider_type + )) + }) + .collect(); + + // Add "Create new Provider" option + items.push( + ListItem::new("Create new Provider") + .style(Style::default().fg(Color::Green)), + ); + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title("Select Provider"), + ) + .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) + .highlight_symbol("> "); + + let mut state = ListState::default(); + state.select(self.selected_provider_index); + + f.render_stateful_widget(list, area, &mut state); + } + + fn render_confirm_create(&self, f: &mut Frame, area: Rect) { + let mut items = + vec![ListItem::new(format!("Name: {}", self.new_profile_name))]; + + if let Some(config) = &self.selected_provider { + items.push(ListItem::new(format!( + "Provider: {}", + config.provider_type + ))); + if let Some(model) = &config.model_identifier { + items.push(ListItem::new(format!("Model: {}", model))); + } + for (key, setting) in &config.additional_settings { + items + .push(ListItem::new(format!("{}: {}", key, setting.value))); + } + } else { + items.push(ListItem::new("No provider selected")); + } + + let confirm_list = List::new(items).block( + Block::default() + .borders(Borders::ALL) + .title("Confirm Profile Creation"), + ); + f.render_widget(confirm_list, area); + } + + fn render_creating_profile(&self, f: &mut Frame, area: Rect) { + let content = + format!("Creating profile '{}'...", self.new_profile_name); + let paragraph = Paragraph::new(content) + .style(Style::default().fg(Color::Green)) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title("Creating Profile"), + ); + f.render_widget(paragraph, area); + } +} + +impl Creator for ProfileCreator { + fn render(&self, f: &mut Frame, area: Rect) { + self.render(f, area) + } +} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_list.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/list.rs similarity index 58% rename from lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_list.rs rename to lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/list.rs index 4e0ec8b..95cc3d6 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/profile_list.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/list.rs @@ -18,43 +18,63 @@ impl ProfileList { } } + pub fn is_default_profile(&self, profile: &UserProfile) -> bool { + self.default_profile + .as_ref() + .map_or(false, |default| default.id == profile.id) + } + + pub fn get_items(&self) -> Vec { + let mut items: Vec = self + .profiles + .iter() + .map(|p| { + if self.is_default_profile(p) { + format!("{} (default)", p.name) + } else { + p.name.clone() + } + }) + .collect(); + + items.push("Create new Profile".to_string()); + items + } + + pub fn is_new_profile_selected(&self) -> bool { + self.selected_index == self.profiles.len() + } + pub fn get_selected_profile(&self) -> Option<&UserProfile> { self.profiles.get(self.selected_index) } - pub fn select_new_profile(&mut self) { - self.selected_index = self.profiles.len(); + pub fn rename_selected_profile(&mut self, new_name: String) { + if let Some(profile) = self.profiles.get_mut(self.selected_index) { + profile.name = new_name; + } } - pub fn move_selection_up(&mut self) { + pub fn move_selection_up(&mut self) -> bool { + let old_index = self.selected_index; if self.selected_index > 0 { self.selected_index -= 1; - } else if self.selected_index == 0 && !self.profiles.is_empty() { - // If at the top and "New Profile" is selected, wrap to the bottom - self.selected_index = self.profiles.len() - 1; + } else { + // Wrap to bottom + self.selected_index = self.profiles.len(); } + old_index != self.selected_index } - pub fn is_new_profile_selected(&self) -> bool { - self.selected_index == self.profiles.len() - } - - pub fn move_selection_down(&mut self) { + pub fn move_selection_down(&mut self) -> bool { + let old_index = self.selected_index; if self.selected_index < self.profiles.len() { self.selected_index += 1; + } else { + // Wrap to top + self.selected_index = 0; } - } - - pub async fn rename_profile( - &mut self, - new_name: String, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError> { - if let Some(profile) = self.profiles.get(self.selected_index) { - db_handler.rename_profile(profile, &new_name).await?; - self.profiles[self.selected_index].name = new_name; - } - Ok(()) + old_index != self.selected_index } pub async fn delete_profile( @@ -73,49 +93,22 @@ impl ProfileList { Ok(()) } - // TODO: this does not update the database - pub fn start_renaming(&self) -> String { - let profile = self.profiles.get(self.selected_index); - if let Some(profile) = profile { - profile.name.clone() - } else { - "".to_string() - } - } - pub fn add_profile(&mut self, profile: UserProfile) { self.profiles.push(profile); self.selected_index = self.profiles.len() - 1; } - pub fn get_selected_index(&self) -> usize { - self.selected_index - } - - pub fn total_items(&self) -> usize { - self.profiles.len() + 1 // +1 for "New Profile" option - } - pub fn mark_as_default(&mut self, profile: &UserProfile) { self.default_profile = Some(profile.clone()); } +} - pub fn is_default_profile(&self, profile: &UserProfile) -> bool { - self.default_profile - .as_ref() - .map_or(false, |default| default == profile) +impl GenericList for ProfileList { + fn get_items(&self) -> Vec { + self.get_items() } - pub fn get_profiles(&self) -> Vec { - self.profiles - .iter() - .map(|p| { - if self.is_default_profile(p) { - format!("* {}", p.name) // Prepend an asterisk to mark the default profile - } else { - p.name.clone() - } - }) - .collect() + fn get_selected_index(&self) -> usize { + self.selected_index } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/manager.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/manager.rs new file mode 100644 index 0000000..aada66f --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/manager.rs @@ -0,0 +1,450 @@ +use serde_json::{json, Map, Value as JsonValue}; +use tokio::sync::mpsc; + +use super::creator::{ProfileCreator, ProfileCreatorAction}; +use super::list::ProfileList; +use super::*; + +pub struct ProfileManager { + pub list: ProfileList, + pub settings_editor: SettingsEditor, + pub creator: Option, + rename_buffer: Option, + db_handler: UserProfileDbHandler, +} + +impl ProfileManager { + pub async fn new( + mut db_handler: UserProfileDbHandler, + ) -> Result { + let profiles = db_handler.list_profiles().await?; + let default_profile = db_handler.get_default_profile().await?; + let list = ProfileList::new(profiles, default_profile); + + let settings = if let Some(profile) = list.get_selected_profile() { + db_handler + .get_profile_settings(profile, MaskMode::Mask) + .await? + } else { + JsonValue::Object(serde_json::Map::new()) + }; + let settings_editor = SettingsEditor::new(settings); + + Ok(Self { + list, + settings_editor, + creator: None, + rename_buffer: None, + db_handler, + }) + } + + pub async fn refresh_profile_list( + &mut self, + ) -> Result { + if let Some(creator) = &mut self.creator { + // Profile is being created in the background + if let Some(ref mut rx) = creator.background_task { + match rx.try_recv() { + Ok(BackgroundTaskResult::ProfileCreated(result)) => { + creator.background_task = None; + creator.task_start_time = None; + match result { + Ok(new_profile) => { + self.list.add_profile(new_profile); + self.creator = None; + } + Err(e) => { + log::error!("Failed to create profile: {}", e); + } + } + return Ok(WindowEvent::Modal(ModalAction::Refresh)); + } + Err(mpsc::error::TryRecvError::Empty) => { + return Ok(WindowEvent::Modal(ModalAction::Refresh)); + } + Err(mpsc::error::TryRecvError::Disconnected) => { + self.creator = None; + return Ok(WindowEvent::Modal(ModalAction::Refresh)); + } + } + } + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + + pub async fn handle_key_event( + &mut self, + key_event: KeyEvent, + tab_focus: &mut TabFocus, + ) -> Result { + match *tab_focus { + TabFocus::List => { + self.handle_list_input(key_event, tab_focus).await + } + TabFocus::Settings => { + self.handle_settings_input(key_event, tab_focus).await + } + TabFocus::Creation => { + self.handle_creation_input(key_event, tab_focus).await + } + } + } + + async fn handle_settings_input( + &mut self, + key_event: KeyEvent, + tab_focus: &mut TabFocus, + ) -> Result { + let (new_mode, handled, action) = + self.settings_editor.handle_key_event(key_event.code); + + if handled { + self.settings_editor.edit_mode = new_mode; + + if let Some(action) = action { + // Get the profile outside the mutable borrow + let profile = self.list.get_selected_profile().cloned(); + + if let Some(profile) = profile { + match action { + SettingsAction::ToggleSecureVisibility => { + self.toggle_secure_visibility(&profile).await?; + } + SettingsAction::DeleteCurrentKey => { + self.delete_current_key(&profile).await?; + } + SettingsAction::ClearCurrentKey => { + self.clear_current_key(&profile).await?; + } + SettingsAction::SaveEdit => { + self.save_edit(&profile).await?; + } + SettingsAction::SaveNewValue => { + self.save_new_value(&profile).await?; + } + } + } + } + + return Ok(WindowEvent::Modal(ModalAction::Refresh)); + } + + if self.settings_editor.edit_mode == EditMode::NotEditing + && (key_event.code == KeyCode::Left + || key_event.code == KeyCode::Char('q') + || key_event.code == KeyCode::Esc + || key_event.code == KeyCode::Tab) + { + *tab_focus = TabFocus::List; + return Ok(WindowEvent::Modal(ModalAction::Refresh)); + } + + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + + async fn toggle_secure_visibility( + &mut self, + profile: &UserProfile, + ) -> Result<(), ApplicationError> { + let new_mask_mode = if self.settings_editor.show_secure { + MaskMode::Mask + } else { + MaskMode::Unmask + }; + let settings = self + .db_handler + .get_profile_settings(profile, new_mask_mode) + .await?; + self.settings_editor.load_settings(settings); + self.settings_editor.show_secure = !self.settings_editor.show_secure; + Ok(()) + } + + async fn delete_current_key( + &mut self, + profile: &UserProfile, + ) -> Result<(), ApplicationError> { + if let Some(current_key) = self.settings_editor.get_current_key() { + if !current_key.starts_with("__") { + let mut settings = Map::new(); + settings.insert(current_key.to_string(), JsonValue::Null); // Null indicates deletion + self.db_handler + .update(profile, &JsonValue::Object(settings)) + .await?; + self.load_profile_settings(profile).await?; + } + } + Ok(()) + } + + async fn clear_current_key( + &mut self, + profile: &UserProfile, + ) -> Result<(), ApplicationError> { + if let Some(current_key) = self.settings_editor.get_current_key() { + if !current_key.starts_with("__") { + let mut settings = self.settings_editor.get_settings().clone(); + settings[current_key] = JsonValue::String("".to_string()); + self.db_handler.update(profile, &settings).await?; + self.load_profile_settings(profile).await?; + } + } + Ok(()) + } + + async fn save_edit( + &mut self, + profile: &UserProfile, + ) -> Result<(), ApplicationError> { + if let Some(current_key) = self.settings_editor.get_current_key() { + let current_value = + &self.settings_editor.get_settings()[current_key]; + let is_encrypted = if let Some(obj) = current_value.as_object() { + obj.contains_key("was_encrypted") + && obj["was_encrypted"].as_bool().unwrap_or(false) + } else { + false + }; + + let new_value = if is_encrypted { + json!({ + "content": self.settings_editor.get_edit_buffer(), + "encryption_key": "", // signal that the value must be encrypted + "type_info": "string", + }) + } else { + serde_json::Value::String( + self.settings_editor.get_edit_buffer().to_string(), + ) + }; + + let mut update_settings = JsonValue::Object(serde_json::Map::new()); + update_settings[current_key] = new_value; + self.db_handler.update(profile, &update_settings).await?; + + self.load_profile_settings(profile).await?; + } + Ok(()) + } + + async fn save_new_value( + &mut self, + profile: &UserProfile, + ) -> Result<(), ApplicationError> { + let new_key = self.settings_editor.get_new_key_buffer().to_string(); + let new_value = if self.settings_editor.is_new_value_secure() { + json!({ + "content": self.settings_editor.get_edit_buffer(), + "encryption_key": "", // signal that value must be encrypted, encryption key will be set by the handler + "type_info": "string", + }) + } else { + serde_json::Value::String( + self.settings_editor.get_edit_buffer().to_string(), + ) + }; + + let mut update_settings = JsonValue::Object(serde_json::Map::new()); + update_settings[&new_key] = new_value.clone(); + self.db_handler.update(profile, &update_settings).await?; + + // Update the local settings for feedback to the user + if let Some(obj) = + self.settings_editor.get_settings_mut().as_object_mut() + { + obj.insert(new_key.clone(), new_value); + } + + // Reload settings to reflect changes + self.load_profile_settings(profile).await?; + + Ok(()) + } + + async fn load_profile_settings( + &mut self, + profile: &UserProfile, + ) -> Result<(), ApplicationError> { + let settings = self + .db_handler + .get_profile_settings(profile, MaskMode::Mask) + .await?; + self.settings_editor.load_settings(settings); + Ok(()) + } + async fn handle_list_input( + &mut self, + key_event: KeyEvent, + tab_focus: &mut TabFocus, + ) -> Result { + match key_event.code { + KeyCode::Up => { + if self.rename_buffer.is_some() { + self.confirm_rename_profile().await?; + } + if self.list.move_selection_up() { + self.load_selected_profile_settings().await?; + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Down => { + if self.rename_buffer.is_some() { + self.confirm_rename_profile().await?; + } + if self.list.move_selection_down() { + self.load_selected_profile_settings().await?; + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Enter => { + if self.list.is_new_profile_selected() { + self.start_profile_creation().await?; + *tab_focus = TabFocus::Creation; + } else if self.rename_buffer.is_some() { + self.confirm_rename_profile().await?; + } else { + *tab_focus = TabFocus::Settings; + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Char('r') | KeyCode::Char('R') => { + self.start_profile_renaming(); + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Char(c) if self.rename_buffer.is_some() => { + if let Some(buffer) = &mut self.rename_buffer { + buffer.push(c); + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Backspace if self.rename_buffer.is_some() => { + if let Some(buffer) = &mut self.rename_buffer { + buffer.pop(); + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Esc if self.rename_buffer.is_some() => { + self.cancel_rename_profile(); + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Char(' ') => { + self.set_default_profile().await?; + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Char('D') => { + self.delete_selected_profile().await?; + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + _ => Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)), + } + } + + pub async fn handle_creation_input( + &mut self, + key_event: KeyEvent, + tab_focus: &mut TabFocus, + ) -> Result { + if let Some(creator) = &mut self.creator { + match creator.handle_input(key_event).await? { + ProfileCreatorAction::Refresh => { + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + ProfileCreatorAction::WaitForKeyEvent => { + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + ProfileCreatorAction::Cancel => { + self.creator = None; + *tab_focus = TabFocus::List; + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + ProfileCreatorAction::CreateProfile => { + let new_profile = + creator.create_profile(&mut self.db_handler).await?; + self.list.add_profile(new_profile); + self.creator = None; + *tab_focus = TabFocus::List; + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + ProfileCreatorAction::SwitchToProviderCreation => { + // This action is handled within the ProfileCreator + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + ProfileCreatorAction::FinishProviderCreation(new_config) => { + // Update the ProviderManager with the new config + // This might require passing a reference to ProviderManager to ProfileManager + // For now, we'll just refresh + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + } + } else { + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + } + + async fn load_selected_profile_settings( + &mut self, + ) -> Result<(), ApplicationError> { + if let Some(profile) = self.list.get_selected_profile() { + let settings = self + .db_handler + .get_profile_settings(profile, MaskMode::Mask) + .await?; + self.settings_editor.load_settings(settings); + } else { + // Clear settings when "Create new Profile" is selected + self.settings_editor.clear(); + } + Ok(()) + } + + async fn start_profile_creation(&mut self) -> Result<(), ApplicationError> { + self.creator = + Some(ProfileCreator::new(self.db_handler.clone()).await?); + Ok(()) + } + + fn start_profile_renaming(&mut self) { + if let Some(profile) = self.list.get_selected_profile() { + self.rename_buffer = Some(profile.name.clone()); + } + } + + async fn confirm_rename_profile(&mut self) -> Result<(), ApplicationError> { + if let (Some(new_name), Some(profile)) = + (&self.rename_buffer, self.list.get_selected_profile()) + { + if !new_name.is_empty() { + self.db_handler.rename_profile(profile, new_name).await?; + self.list.rename_selected_profile(new_name.clone()); + } + } + self.rename_buffer = None; + Ok(()) + } + + fn cancel_rename_profile(&mut self) { + self.rename_buffer = None; + } + + async fn set_default_profile(&mut self) -> Result<(), ApplicationError> { + if let Some(profile) = self.list.get_selected_profile().cloned() { + self.db_handler.set_default_profile(&profile).await?; + self.list.mark_as_default(&profile); + } + Ok(()) + } + + async fn delete_selected_profile( + &mut self, + ) -> Result<(), ApplicationError> { + self.list.delete_profile(&mut self.db_handler).await?; + self.load_selected_profile_settings().await?; + self.rename_buffer = None; + Ok(()) + } + + pub fn get_rename_buffer(&self) -> Option<&String> { + self.rename_buffer.as_ref() + } +} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/mod.rs new file mode 100644 index 0000000..3fc6055 --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/mod.rs @@ -0,0 +1,9 @@ +mod creator; +mod list; +mod manager; +mod renderer; + +pub use manager::ProfileManager; +pub use renderer::ProfileEditRenderer; + +use super::*; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/renderer.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/renderer.rs new file mode 100644 index 0000000..015f2c1 --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/renderer.rs @@ -0,0 +1,266 @@ +use ratatui::prelude::*; +use ratatui::widgets::{ + Block, Borders, HighlightSpacing, List, ListItem, ListState, Paragraph, + Tabs, +}; + +use super::{EditMode, EditTab, SettingsModal, SimpleString, TabFocus}; + +pub struct ProfileEditRenderer; + +impl ProfileEditRenderer { + pub fn new() -> Self { + ProfileEditRenderer + } + + pub fn render_layout( + &self, + f: &mut Frame, + area: Rect, + modal: &SettingsModal, + ) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Tab bar + Constraint::Min(1), // Main content + Constraint::Length(3), // Instructions + ]) + .split(area); + + self.render_tab_bar(f, chunks[0], modal.current_tab); + + let main_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(30), + Constraint::Percentage(70), + ]) + .split(chunks[1]); + + self.render_list(f, main_chunks[0], modal); + self.render_content(f, main_chunks[1], modal); + + self.render_instructions(f, chunks[2], modal); + } + + fn render_tab_bar(&self, f: &mut Frame, area: Rect, current_tab: EditTab) { + let tabs = vec!["Profiles", "Providers"]; + let tab_index = match current_tab { + EditTab::Profiles => 0, + EditTab::Providers => 1, + }; + let tabs = Tabs::new(tabs) + .block(Block::default().borders(Borders::ALL)) + .select(tab_index) + .style(Style::default().fg(Color::Cyan)) + .highlight_style(Style::default().fg(Color::Yellow)); + f.render_widget(tabs, area); + } + + fn render_list(&self, f: &mut Frame, area: Rect, modal: &SettingsModal) { + let items: Vec = modal + .get_current_list() + .get_items() + .into_iter() + .enumerate() + .map(|(i, item)| { + let content = if i + == modal.get_current_list().get_selected_index() + && modal.get_rename_buffer().is_some() + { + // Show rename buffer for the selected item if renaming + format!("{}", modal.get_rename_buffer().unwrap()) + } else { + item.clone() + }; + + let style = if i + == modal.get_current_list().get_selected_index() + { + Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White) + } else if i == modal.get_current_list().get_items().len() - 1 { + // Special style for "Create new Profile" + Style::default().fg(Color::Green) + } else if item.ends_with("(default)") { + // Special style for default profile + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Cyan) + }; + ListItem::new(Span::styled(content, style)) + }) + .collect(); + + let title = match modal.current_tab { + EditTab::Profiles => "Profiles", + EditTab::Providers => "Providers", + }; + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title(title)) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol("> ") + .highlight_spacing(HighlightSpacing::Always); + + let mut list_state = ListState::default(); + list_state.select(Some(modal.get_current_list().get_selected_index())); + + f.render_stateful_widget(list, area, &mut list_state); + } + + fn render_content(&self, f: &mut Frame, area: Rect, modal: &SettingsModal) { + match modal.tab_focus { + TabFocus::Settings | TabFocus::List => { + self.render_settings(f, area, modal) + } + TabFocus::Creation => { + if let Some(creator) = modal.get_current_creator() { + creator.render(f, area); + } + } + } + } + + fn render_settings( + &self, + f: &mut Frame, + area: Rect, + modal: &SettingsModal, + ) { + let settings = modal.get_current_settings_editor().get_settings(); + let mut items: Vec = settings + .as_object() + .unwrap() + .iter() + .enumerate() + .map(|(i, (key, value))| { + let content = format!("{}: {}", key, value); + let style = if i + == modal.get_current_settings_editor().get_current_field() + && modal.tab_focus == TabFocus::Settings + { + Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White) + } else { + Style::default().bg(Color::Black).fg(Color::Cyan) + }; + ListItem::new(Span::styled(content, style)) + }) + .collect(); + + // Add new key input field if in AddingNewKey mode + if matches!( + modal.get_current_settings_editor().edit_mode, + EditMode::AddingNewKey + ) { + let secure_indicator = + if modal.get_current_settings_editor().is_new_value_secure() { + "🔒 " + } else { + "" + }; + items.push(ListItem::new(Span::styled( + format!( + "{}New key: {}", + secure_indicator, + modal.get_current_settings_editor().get_new_key_buffer() + ), + Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White), + ))); + } + + // Add new value input field if in AddingNewValue mode + if matches!( + modal.get_current_settings_editor().edit_mode, + EditMode::AddingNewValue + ) { + let secure_indicator = + if modal.get_current_settings_editor().is_new_value_secure() { + "🔒 " + } else { + "" + }; + items.push(ListItem::new(Span::styled( + format!( + "{}{}: {}", + secure_indicator, + modal.get_current_settings_editor().get_new_key_buffer(), + modal.get_current_settings_editor().get_edit_buffer() + ), + Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White), + ))); + } + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("Settings")) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol("> "); + + let mut list_state = ListState::default(); + if modal.tab_focus == TabFocus::Settings { + list_state.select(Some( + modal.get_current_settings_editor().get_current_field(), + )); + } + + f.render_stateful_widget(list, area, &mut list_state); + } + + fn render_instructions( + &self, + f: &mut Frame, + area: Rect, + modal: &SettingsModal, + ) { + let instructions = match (modal.current_tab, modal.tab_focus) { + (EditTab::Profiles, TabFocus::List) => { + "↑↓: Navigate | Enter: Select | R: Rename | D: Delete | Space: \ + Set Default | Tab: Switch Tab" + } + (EditTab::Profiles, TabFocus::Settings) => { + match modal.get_current_settings_editor().edit_mode { + EditMode::NotEditing => { + "↑↓: Navigate | Enter: Edit | n: New | N: New Secure | \ + D: Delete | C: Clear | S: Show/Hide Secure | \ + ←/Tab/q/Esc: Back to List" + } + EditMode::EditingValue => "Enter: Save | Esc: Cancel", + EditMode::AddingNewKey => { + "Enter: Confirm Key | Esc: Cancel" + } + EditMode::AddingNewValue => { + "Enter: Save New Value | Esc: Cancel" + } + } + } + (EditTab::Profiles, TabFocus::Creation) => { + "Enter: Create Profile | Esc: Cancel" + } + (EditTab::Providers, TabFocus::List) => { + "↑↓: Navigate | Enter: Select | D: Delete | Tab: Switch Tab" + } + (EditTab::Providers, TabFocus::Settings) => { + "↑↓: Navigate | Enter: Edit | D: Delete | Esc: Back to List" + } + (EditTab::Providers, TabFocus::Creation) => { + "Enter: Create Provider | Esc: Cancel" + } + }; + + let simple_string = SimpleString::from(instructions); + let wrapped_instructions = simple_string.wrapped_spans( + area.width as usize - 2, // Subtracting 2 for borders + Some(Style::default().fg(Color::Cyan)), + Some(" | "), + ); + + let wrapped_text: Vec = + wrapped_instructions.into_iter().map(Line::from).collect(); + + let paragraph = Paragraph::new(wrapped_text) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::TOP)); + + f.render_widget(paragraph, area); + } +} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/provider_manager.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/creator.rs similarity index 76% rename from lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/provider_manager.rs rename to lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/creator.rs index ec53d36..cfb4bc6 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/provider_manager.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/creator.rs @@ -1,208 +1,37 @@ use std::collections::HashMap; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::layout::Rect; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; -use ratatui::Frame; -use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use super::*; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProviderConfig { - pub id: Option, - pub name: String, - pub provider_type: String, - pub model_identifier: Option, - pub additional_settings: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AdditionalSetting { - pub name: String, - pub display_name: String, - pub value: String, - pub is_secure: bool, - pub placeholder: String, -} - -pub struct ProviderManager { - configs: Vec, - selected_index: Option, - db_handler: UserProfileDbHandler, - provider_creator: Option, +#[derive(Debug, Clone)] +pub enum ProviderCreatorAction { + Refresh, + WaitForKeyEvent, + LoadModels, + LoadAdditionalSettings, + Finish(ProviderConfig), + Cancel, + NoAction, } -impl ProviderManager { - pub fn new(db_handler: UserProfileDbHandler) -> Self { - Self { - configs: Vec::new(), - selected_index: None, - db_handler, - provider_creator: None, - } - } - - pub fn render(&self, f: &mut Frame, area: Rect) { - if let Some(creator) = &self.provider_creator { - creator.render(f, area); - } else { - self.render_provider_list(f, area); - } - } - - fn render_provider_list(&self, f: &mut Frame, area: Rect) { - let mut items = Vec::new(); - - if self.configs.is_empty() { - items.push(ListItem::new("No providers configured")); - } else { - for (index, config) in self.configs.iter().enumerate() { - let content = - format!("{}: {}", config.name, config.provider_type); - let style = if Some(index) == self.selected_index { - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD) - } else { - Style::default() - }; - items.push(ListItem::new(content).style(style)); - } - } - - items.push( - ListItem::new("Create New Provider") - .style(Style::default().fg(Color::Green)), - ); - - let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Select or Create Provider"), - ) - .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) - .highlight_symbol("> "); - - let mut state = ListState::default(); - state.select(self.selected_index); - - f.render_stateful_widget(list, area, &mut state); - } - - pub async fn handle_input( - &mut self, - input: KeyEvent, - ) -> Result { - if let Some(creator) = &mut self.provider_creator { - match creator.handle_input(input).await { - ProviderCreatorAction::Finish(new_config) => { - self.configs.push(new_config); - self.provider_creator = None; - Ok(ProviderManagerAction::Refresh) - } - ProviderCreatorAction::Cancel => { - self.provider_creator = None; - Ok(ProviderManagerAction::Refresh) - } - ProviderCreatorAction::LoadModels => { - creator.load_models().await?; - Ok(ProviderManagerAction::Refresh) - } - ProviderCreatorAction::LoadAdditionalSettings => { - let model_server = - ModelServer::from_str(&creator.provider_type)?; - creator.prepare_additional_settings(&model_server); - Ok(ProviderManagerAction::Refresh) - } - ProviderCreatorAction::Refresh => { - Ok(ProviderManagerAction::Refresh) - } - ProviderCreatorAction::NoAction => { - Ok(ProviderManagerAction::NoAction) - } - } - } else { - match input.code { - KeyCode::Up => Ok(self.move_selection_up()), - KeyCode::Down => Ok(self.move_selection_down()), - KeyCode::Enter => Ok(self.select_or_create_provider()), - _ => Ok(ProviderManagerAction::NoAction), - } - } - } - - fn move_selection_up(&mut self) -> ProviderManagerAction { - if self.configs.is_empty() { - return ProviderManagerAction::NoAction; - } - if let Some(index) = self.selected_index.as_mut() { - if *index > 0 { - *index -= 1; - } else { - *index = self.configs.len(); // Wrap to "Create New Provider" - } - } else { - self.selected_index = Some(self.configs.len()); // Select "Create New Provider" - } - ProviderManagerAction::Refresh - } - - fn move_selection_down(&mut self) -> ProviderManagerAction { - if self.configs.is_empty() { - return ProviderManagerAction::NoAction; - } - if let Some(index) = self.selected_index.as_mut() { - if *index < self.configs.len() { - *index += 1; - } else { - *index = 0; // Wrap to first provider - } - } else { - self.selected_index = Some(0); - } - ProviderManagerAction::Refresh - } - - fn select_or_create_provider(&mut self) -> ProviderManagerAction { - if self.configs.is_empty() - || self.selected_index == Some(self.configs.len()) - { - self.provider_creator = - Some(ProviderCreator::new(self.db_handler.clone())); - ProviderManagerAction::Refresh - } else if self.selected_index.is_some() { - ProviderManagerAction::ProviderSelected - } else { - ProviderManagerAction::NoAction - } - } - - pub fn get_selected_provider(&self) -> Option<&ProviderConfig> { - self.selected_index - .and_then(|index| self.configs.get(index)) - } - - pub async fn load_configs(&mut self) -> Result<(), ApplicationError> { - self.configs = self.db_handler.load_provider_configs().await?; - if !self.configs.is_empty() { - self.selected_index = Some(0); - } else { - self.selected_index = None; - } - Ok(()) - } +#[derive(Debug, Clone, PartialEq)] +enum ProviderCreationStep { + EnterName, + SelectProviderType, + SelectModel, + ConfigureSettings, + Confirm, } pub struct ProviderCreator { name: String, - provider_type: String, + pub provider_type: String, model_identifier: Option, - additional_settings: HashMap, + additional_settings: HashMap, db_handler: UserProfileDbHandler, current_step: ProviderCreationStep, available_models: Vec, @@ -213,18 +42,11 @@ pub struct ProviderCreator { model_fetch_error: Option, } -#[derive(Debug, Clone, PartialEq)] -enum ProviderCreationStep { - EnterName, - SelectProviderType, - SelectModel, - ConfigureSettings, - Confirm, -} - impl ProviderCreator { - pub fn new(db_handler: UserProfileDbHandler) -> Self { - Self { + pub async fn new( + db_handler: UserProfileDbHandler, + ) -> Result { + Ok(Self { name: String::new(), provider_type: String::new(), model_identifier: None, @@ -237,7 +59,7 @@ impl ProviderCreator { edit_buffer: String::new(), is_editing: false, model_fetch_error: None, - } + }) } pub fn render(&self, f: &mut Frame, area: Rect) { @@ -783,7 +605,7 @@ impl ProviderCreator { .to_string(); self.additional_settings.insert( key.clone(), - AdditionalSetting { + ProviderConfigOptions { name: format!("__TEMPLATE.{}", key), display_name, value: String::new(), @@ -804,7 +626,7 @@ impl ProviderCreator { } } - async fn create_provider( + pub async fn create_provider( &mut self, ) -> Result { let new_config = ProviderConfig { @@ -814,25 +636,14 @@ impl ProviderCreator { model_identifier: self.model_identifier.clone(), additional_settings: self.additional_settings.clone(), }; - - _ = self.db_handler.save_provider_config(&new_config).await?; - Ok(new_config) + let stored_config = + self.db_handler.save_provider_config(&new_config).await?; + Ok(stored_config) } } -#[derive(Debug, Clone)] -pub enum ProviderCreatorAction { - Refresh, - LoadModels, - LoadAdditionalSettings, - Finish(ProviderConfig), - Cancel, - NoAction, -} - -#[derive(Debug)] -pub enum ProviderManagerAction { - Refresh, - ProviderSelected, - NoAction, +impl Creator for ProviderCreator { + fn render(&self, f: &mut Frame, area: Rect) { + self.render(f, area); + } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/list.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/list.rs new file mode 100644 index 0000000..60b037d --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/list.rs @@ -0,0 +1,89 @@ +use super::{ + ApplicationError, GenericList, ProviderConfig, UserProfileDbHandler, +}; + +pub struct ProviderList { + providers: Vec, + selected_index: usize, +} + +impl ProviderList { + pub fn new(providers: Vec) -> Self { + ProviderList { + providers, + selected_index: 0, + } + } + + pub fn get_selected_provider(&self) -> Option<&ProviderConfig> { + self.providers.get(self.selected_index) + } + + pub fn is_new_provider_selected(&self) -> bool { + self.selected_index == self.providers.len() + } + + pub fn move_selection_up(&mut self) -> bool { + let old_index = self.selected_index; + if self.selected_index > 0 { + self.selected_index -= 1; + } else { + // Wrap to bottom + self.selected_index = self.providers.len(); + } + old_index != self.selected_index + } + + pub fn move_selection_down(&mut self) -> bool { + let old_index = self.selected_index; + if self.selected_index < self.providers.len() { + self.selected_index += 1; + } else { + // Wrap to top + self.selected_index = 0; + } + old_index != self.selected_index + } + + pub async fn delete_provider( + &mut self, + db_handler: &mut UserProfileDbHandler, + ) -> Result<(), ApplicationError> { + if let Some(provider) = self.providers.get(self.selected_index) { + if let Some(id) = provider.id { + db_handler.delete_provider_config(id).await?; + self.providers.remove(self.selected_index); + if self.selected_index >= self.providers.len() + && !self.providers.is_empty() + { + self.selected_index = self.providers.len() - 1; + } + } + } + Ok(()) + } + + pub fn add_provider(&mut self, provider: ProviderConfig) { + self.providers.push(provider); + self.selected_index = self.providers.len() - 1; + } + + pub fn rename_selected_provider(&mut self, new_name: String) { + if let Some(provider) = self.providers.get_mut(self.selected_index) { + provider.name = new_name; + } + } +} + +impl GenericList for ProviderList { + fn get_items(&self) -> Vec { + let mut items: Vec = + self.providers.iter().map(|p| p.name.clone()).collect(); + items.push("Create new Provider".to_string()); + items + } + + fn get_selected_index(&self) -> usize { + self.selected_index + } +} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/manager.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/manager.rs new file mode 100644 index 0000000..63991f8 --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/manager.rs @@ -0,0 +1,399 @@ +use serde_json::Value as JsonValue; + +use super::list::ProviderList; +use super::*; +pub struct ProviderManager { + pub list: ProviderList, + pub settings_editor: SettingsEditor, + pub creator: Option, + rename_buffer: Option, + db_handler: UserProfileDbHandler, + renderer: ProviderEditRenderer, + pub tab_focus: TabFocus, +} + +impl ProviderManager { + pub async fn new( + db_handler: UserProfileDbHandler, + ) -> Result { + let providers = db_handler.load_provider_configs().await?; + let list = ProviderList::new(providers); + + let settings = if let Some(provider) = list.get_selected_provider() { + JsonValue::Object(serde_json::Map::from_iter( + provider.additional_settings.iter().map(|(k, v)| { + (k.clone(), JsonValue::String(v.value.clone())) + }), + )) + } else { + JsonValue::Object(serde_json::Map::new()) + }; + let settings_editor = SettingsEditor::new(settings); + + Ok(Self { + list, + settings_editor, + creator: None, + rename_buffer: None, + db_handler, + renderer: ProviderEditRenderer::new(), + tab_focus: TabFocus::List, + }) + } + + pub fn render(&self, f: &mut Frame, area: Rect) { + self.renderer.render_layout(f, area, self); + } + + pub async fn refresh_provider_list( + &mut self, + ) -> Result { + let providers = self.db_handler.load_provider_configs().await?; + self.list = ProviderList::new(providers); + self.load_selected_provider_settings().await?; + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + + pub async fn handle_key_event( + &mut self, + key_event: KeyEvent, + tab_focus: &mut TabFocus, + ) -> Result { + match *tab_focus { + TabFocus::List => { + self.handle_list_input(key_event, tab_focus).await + } + TabFocus::Settings => { + self.handle_settings_input(key_event, tab_focus).await + } + TabFocus::Creation => { + self.handle_creation_input(key_event, tab_focus).await + } + } + } + + async fn handle_list_input( + &mut self, + key_event: KeyEvent, + tab_focus: &mut TabFocus, + ) -> Result { + match key_event.code { + KeyCode::Up => { + if self.rename_buffer.is_some() { + self.confirm_rename_provider().await?; + } + if self.list.move_selection_up() { + self.load_selected_provider_settings().await?; + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Down => { + if self.rename_buffer.is_some() { + self.confirm_rename_provider().await?; + } + if self.list.move_selection_down() { + self.load_selected_provider_settings().await?; + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Enter => { + if self.list.is_new_provider_selected() { + self.start_provider_creation().await?; + *tab_focus = TabFocus::Creation; + } else if self.rename_buffer.is_some() { + self.confirm_rename_provider().await?; + } else { + *tab_focus = TabFocus::Settings; + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Char('r') | KeyCode::Char('R') => { + self.start_provider_renaming(); + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Char(c) if self.rename_buffer.is_some() => { + if let Some(buffer) = &mut self.rename_buffer { + buffer.push(c); + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Backspace if self.rename_buffer.is_some() => { + if let Some(buffer) = &mut self.rename_buffer { + buffer.pop(); + } + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Esc if self.rename_buffer.is_some() => { + self.cancel_rename_provider(); + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + KeyCode::Char('D') => { + self.delete_selected_provider().await?; + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + _ => Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)), + } + } + + async fn handle_settings_input( + &mut self, + key_event: KeyEvent, + tab_focus: &mut TabFocus, + ) -> Result { + let (new_mode, handled, action) = + self.settings_editor.handle_key_event(key_event.code); + + if handled { + self.settings_editor.edit_mode = new_mode; + + if let Some(action) = action { + // Get the provider outside the mutable borrow + let provider = self.list.get_selected_provider().cloned(); + + if let Some(provider) = provider { + match action { + SettingsAction::ToggleSecureVisibility => { + self.toggle_secure_visibility(&provider).await?; + } + SettingsAction::DeleteCurrentKey => { + self.delete_current_key(&provider).await?; + } + SettingsAction::ClearCurrentKey => { + self.clear_current_key(&provider).await?; + } + SettingsAction::SaveEdit => { + self.save_edit(&provider).await?; + } + SettingsAction::SaveNewValue => { + self.save_new_value(&provider).await?; + } + } + } + } + + return Ok(WindowEvent::Modal(ModalAction::Refresh)); + } + + if self.settings_editor.edit_mode == EditMode::NotEditing + && (key_event.code == KeyCode::Left + || key_event.code == KeyCode::Char('q') + || key_event.code == KeyCode::Esc + || key_event.code == KeyCode::Tab) + { + *tab_focus = TabFocus::List; + return Ok(WindowEvent::Modal(ModalAction::Refresh)); + } + + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + + async fn handle_creation_input( + &mut self, + key_event: KeyEvent, + tab_focus: &mut TabFocus, + ) -> Result { + if let Some(creator) = &mut self.creator { + match creator.handle_input(key_event).await { + ProviderCreatorAction::Refresh => { + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + ProviderCreatorAction::WaitForKeyEvent => { + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + ProviderCreatorAction::Cancel => { + self.creator = None; + *tab_focus = TabFocus::List; + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + ProviderCreatorAction::Finish(new_provider) => { + self.list.add_provider(new_provider); + self.creator = None; + *tab_focus = TabFocus::List; + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + ProviderCreatorAction::LoadModels => { + creator.load_models().await?; + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + ProviderCreatorAction::LoadAdditionalSettings => { + let model_server = + ModelServer::from_str(&creator.provider_type)?; + creator.prepare_additional_settings(&model_server); + Ok(WindowEvent::Modal(ModalAction::Refresh)) + } + ProviderCreatorAction::NoAction => { + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + } + } else { + Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) + } + } + + async fn load_selected_provider_settings( + &mut self, + ) -> Result<(), ApplicationError> { + if let Some(provider) = self.list.get_selected_provider() { + let settings = JsonValue::Object(serde_json::Map::from_iter( + provider.additional_settings.iter().map(|(k, v)| { + (k.clone(), JsonValue::String(v.value.clone())) + }), + )); + self.settings_editor.load_settings(settings); + } else { + // Clear settings when "Create new Provider" is selected + self.settings_editor.clear(); + } + Ok(()) + } + + async fn start_provider_creation( + &mut self, + ) -> Result<(), ApplicationError> { + self.creator = + Some(ProviderCreator::new(self.db_handler.clone()).await?); + Ok(()) + } + + fn start_provider_renaming(&mut self) { + if let Some(provider) = self.list.get_selected_provider() { + self.rename_buffer = Some(provider.name.clone()); + } + } + + async fn confirm_rename_provider( + &mut self, + ) -> Result<(), ApplicationError> { + if let (Some(new_name), Some(provider)) = + (&self.rename_buffer, self.list.get_selected_provider()) + { + if !new_name.is_empty() { + let mut updated_provider = provider.clone(); + updated_provider.name = new_name.clone(); + self.db_handler + .save_provider_config(&updated_provider) + .await?; + self.list.rename_selected_provider(new_name.clone()); + } + } + self.rename_buffer = None; + Ok(()) + } + + fn cancel_rename_provider(&mut self) { + self.rename_buffer = None; + } + + async fn delete_selected_provider( + &mut self, + ) -> Result<(), ApplicationError> { + self.list.delete_provider(&mut self.db_handler).await?; + self.load_selected_provider_settings().await?; + self.rename_buffer = None; + Ok(()) + } + + pub fn get_rename_buffer(&self) -> Option<&String> { + self.rename_buffer.as_ref() + } + + async fn toggle_secure_visibility( + &mut self, + provider: &ProviderConfig, + ) -> Result<(), ApplicationError> { + if let Some(current_key) = self.settings_editor.get_current_key() { + let mut updated_provider = provider.clone(); + if let Some(setting) = + updated_provider.additional_settings.get_mut(current_key) + { + setting.is_secure = !setting.is_secure; + } + self.db_handler + .save_provider_config(&updated_provider) + .await?; + self.load_selected_provider_settings().await?; + } + Ok(()) + } + + async fn delete_current_key( + &mut self, + provider: &ProviderConfig, + ) -> Result<(), ApplicationError> { + if let Some(current_key) = self.settings_editor.get_current_key() { + let mut updated_provider = provider.clone(); + updated_provider.additional_settings.remove(current_key); + self.db_handler + .save_provider_config(&updated_provider) + .await?; + self.load_selected_provider_settings().await?; + } + Ok(()) + } + + async fn clear_current_key( + &mut self, + provider: &ProviderConfig, + ) -> Result<(), ApplicationError> { + if let Some(current_key) = self.settings_editor.get_current_key() { + let mut updated_provider = provider.clone(); + if let Some(setting) = + updated_provider.additional_settings.get_mut(current_key) + { + setting.value.clear(); + } + self.db_handler + .save_provider_config(&updated_provider) + .await?; + self.load_selected_provider_settings().await?; + } + Ok(()) + } + + async fn save_edit( + &mut self, + provider: &ProviderConfig, + ) -> Result<(), ApplicationError> { + if let Some(current_key) = self.settings_editor.get_current_key() { + let mut updated_provider = provider.clone(); + if let Some(setting) = + updated_provider.additional_settings.get_mut(current_key) + { + setting.value = + self.settings_editor.get_edit_buffer().to_string(); + } + self.db_handler + .save_provider_config(&updated_provider) + .await?; + self.load_selected_provider_settings().await?; + } + Ok(()) + } + + async fn save_new_value( + &mut self, + provider: &ProviderConfig, + ) -> Result<(), ApplicationError> { + let new_key = self.settings_editor.get_new_key_buffer().to_string(); + let new_value = self.settings_editor.get_edit_buffer().to_string(); + + let mut updated_provider = provider.clone(); + updated_provider.additional_settings.insert( + new_key.clone(), + ProviderConfigOptions { + name: new_key.clone(), + display_name: new_key.clone(), + value: new_value, + is_secure: false, + placeholder: String::new(), + }, + ); + + self.db_handler + .save_provider_config(&updated_provider) + .await?; + self.load_selected_provider_settings().await?; + + Ok(()) + } +} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/mod.rs new file mode 100644 index 0000000..21cd35c --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/mod.rs @@ -0,0 +1,10 @@ +mod creator; +mod list; +mod manager; +mod renderer; + +pub use creator::{ProviderCreator, ProviderCreatorAction}; +pub use manager::ProviderManager; +pub use renderer::ProviderEditRenderer; + +use super::*; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/renderer.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/renderer.rs new file mode 100644 index 0000000..d06990b --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/renderer.rs @@ -0,0 +1,229 @@ +use ratatui::prelude::*; +use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; + +use super::manager::ProviderManager; +use super::{EditMode, GenericList, TabFocus}; + +pub struct ProviderEditRenderer; + +impl ProviderEditRenderer { + pub fn new() -> Self { + ProviderEditRenderer + } + + pub fn render_layout( + &self, + f: &mut Frame, + area: Rect, + provider_manager: &ProviderManager, + ) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Title + Constraint::Min(1), // Main content + Constraint::Length(3), // Instructions + ]) + .split(area); + + self.render_title(f, chunks[0]); + + let main_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(30), + Constraint::Percentage(70), + ]) + .split(chunks[1]); + + self.render_provider_list(f, main_chunks[0], provider_manager); + self.render_content(f, main_chunks[1], provider_manager); + + self.render_instructions(f, chunks[2], provider_manager); + } + + fn render_title(&self, f: &mut Frame, area: Rect) { + let title = Paragraph::new("Provider Editor") + .style(Style::default().fg(Color::Cyan)) + .alignment(Alignment::Center); + f.render_widget(title, area); + } + + fn render_provider_list( + &self, + f: &mut Frame, + area: Rect, + provider_manager: &ProviderManager, + ) { + let items: Vec = provider_manager + .list + .get_items() + .into_iter() + .enumerate() + .map(|(i, item)| { + let content = if i == provider_manager.list.get_selected_index() + && provider_manager.get_rename_buffer().is_some() + { + format!("{}", provider_manager.get_rename_buffer().unwrap()) + } else { + item.clone() + }; + + let style = if i == provider_manager.list.get_selected_index() { + Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White) + } else if i == provider_manager.list.get_items().len() - 1 { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::Cyan) + }; + ListItem::new(Span::styled(content, style)) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("Providers")) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol("> "); + + let mut list_state = ListState::default(); + list_state.select(Some(provider_manager.list.get_selected_index())); + + f.render_stateful_widget(list, area, &mut list_state); + } + + fn render_content( + &self, + f: &mut Frame, + area: Rect, + provider_manager: &ProviderManager, + ) { + match provider_manager.tab_focus { + TabFocus::Settings | TabFocus::List => { + self.render_settings(f, area, provider_manager) + } + TabFocus::Creation => { + if let Some(creator) = &provider_manager.creator { + creator.render(f, area); + } + } + } + } + + fn render_settings( + &self, + f: &mut Frame, + area: Rect, + provider_manager: &ProviderManager, + ) { + if let Some(provider) = provider_manager.list.get_selected_provider() { + let mut items = Vec::new(); + + items.push(ListItem::new(format!("Name: {}", provider.name))); + items.push(ListItem::new(format!( + "Type: {}", + provider.provider_type + ))); + + if let Some(model) = &provider.model_identifier { + items.push(ListItem::new(format!("Model: {}", model))); + } else { + items.push(ListItem::new("Model: Not set")); + } + + items.push(ListItem::new("Additional Settings:")); + for (key, setting) in &provider.additional_settings { + let value_display = if setting.is_secure { + "*".repeat(setting.value.len()) + } else { + setting.value.clone() + }; + + let content = + format!(" {}: {}", setting.display_name, value_display); + let style = + if provider_manager.settings_editor.get_current_key() + == Some(key.as_str()) + { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + items.push(ListItem::new(Span::styled(content, style))); + } + + if let EditMode::EditingValue = + provider_manager.settings_editor.edit_mode + { + if let Some(current_key) = + provider_manager.settings_editor.get_current_key() + { + let edit_content = format!( + "Editing {}: {}", + current_key, + provider_manager.settings_editor.get_edit_buffer() + ); + items.push(ListItem::new(Span::styled( + edit_content, + Style::default().fg(Color::Cyan), + ))); + } + } + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title("Provider Settings"), + ) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol("> "); + + f.render_widget(list, area); + } else { + let paragraph = Paragraph::new("No provider selected").block( + Block::default() + .borders(Borders::ALL) + .title("Provider Settings"), + ); + f.render_widget(paragraph, area); + } + } + + fn render_instructions( + &self, + f: &mut Frame, + area: Rect, + provider_manager: &ProviderManager, + ) { + let instructions = match ( + provider_manager.tab_focus, + provider_manager.settings_editor.edit_mode, + ) { + (TabFocus::List, _) => { + "↑↓: Navigate | Enter: Select | R: Rename | D: Delete | Tab: \ + Switch Tab" + } + (TabFocus::Settings, EditMode::NotEditing) => { + "↑↓: Navigate | Enter: Edit | n: New | N: New Secure | D: \ + Delete | C: Clear | S: Show/Hide Secure | ←/Tab/q/Esc: Back \ + to List" + } + (TabFocus::Settings, EditMode::EditingValue) => { + "Enter: Save | Esc: Cancel" + } + (TabFocus::Settings, EditMode::AddingNewKey) => { + "Enter: Confirm Key | Esc: Cancel" + } + (TabFocus::Settings, EditMode::AddingNewValue) => { + "Enter: Save New Value | Esc: Cancel" + } + (TabFocus::Creation, _) => "Enter: Create Provider | Esc: Cancel", + }; + + let paragraph = Paragraph::new(instructions) + .style(Style::default().fg(Color::Cyan)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::TOP)); + f.render_widget(paragraph, area); + } +} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/settings_editor.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/settings_editor.rs new file mode 100644 index 0000000..696ace0 --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/settings_editor.rs @@ -0,0 +1,257 @@ +use serde_json::{Map, Value as JsonValue}; + +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SettingsAction { + ToggleSecureVisibility, + DeleteCurrentKey, + ClearCurrentKey, + SaveEdit, + SaveNewValue, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SettingsEditor { + settings: JsonValue, + current_field: usize, + edit_buffer: String, + new_key_buffer: String, + is_new_value_secure: bool, + pub show_secure: bool, + pub edit_mode: EditMode, +} + +impl SettingsEditor { + pub fn new(settings: JsonValue) -> Self { + Self { + settings, + current_field: 0, + edit_buffer: String::new(), + new_key_buffer: String::new(), + is_new_value_secure: false, + show_secure: false, + edit_mode: EditMode::NotEditing, + } + } + + pub fn load_settings(&mut self, settings: JsonValue) { + self.settings = settings; + self.current_field = 0; + self.edit_buffer.clear(); + self.new_key_buffer.clear(); + self.is_new_value_secure = false; + } + + pub fn handle_key_event( + &mut self, + key_code: KeyCode, + ) -> (EditMode, bool, Option) { + match self.edit_mode { + EditMode::NotEditing => match key_code { + KeyCode::Up => { + self.move_selection_up(); + (EditMode::NotEditing, true, None) + } + KeyCode::Down => { + self.move_selection_down(); + (EditMode::NotEditing, true, None) + } + KeyCode::Enter => { + if self.start_editing().is_some() { + (EditMode::EditingValue, true, None) + } else { + (EditMode::NotEditing, false, None) + } + } + KeyCode::Char('n') => { + self.start_adding_new_value(false); + (EditMode::AddingNewKey, true, None) + } + KeyCode::Char('N') => { + self.start_adding_new_value(true); + (EditMode::AddingNewKey, true, None) + } + KeyCode::Char('s') | KeyCode::Char('S') => ( + EditMode::NotEditing, + true, + Some(SettingsAction::ToggleSecureVisibility), + ), + KeyCode::Char('D') => ( + EditMode::NotEditing, + true, + Some(SettingsAction::DeleteCurrentKey), + ), + KeyCode::Char('C') => ( + EditMode::NotEditing, + true, + Some(SettingsAction::ClearCurrentKey), + ), + _ => (EditMode::NotEditing, false, None), + }, + EditMode::EditingValue => match key_code { + KeyCode::Enter => { + (EditMode::NotEditing, true, Some(SettingsAction::SaveEdit)) + } + KeyCode::Esc => { + self.cancel_edit(); + (EditMode::NotEditing, true, None) + } + KeyCode::Backspace => { + self.edit_buffer.pop(); + (EditMode::EditingValue, true, None) + } + KeyCode::Char(c) => { + self.edit_buffer.push(c); + (EditMode::EditingValue, true, None) + } + _ => (EditMode::EditingValue, false, None), + }, + EditMode::AddingNewKey => match key_code { + KeyCode::Enter => { + if self.confirm_new_key() { + (EditMode::AddingNewValue, true, None) + } else { + (EditMode::AddingNewKey, false, None) + } + } + KeyCode::Esc => { + self.cancel_edit(); + (EditMode::NotEditing, true, None) + } + KeyCode::Backspace => { + self.new_key_buffer.pop(); + (EditMode::AddingNewKey, true, None) + } + KeyCode::Char(c) => { + self.new_key_buffer.push(c); + (EditMode::AddingNewKey, true, None) + } + _ => (EditMode::AddingNewKey, false, None), + }, + EditMode::AddingNewValue => match key_code { + KeyCode::Enter => ( + EditMode::NotEditing, + true, + Some(SettingsAction::SaveNewValue), + ), + KeyCode::Esc => { + self.cancel_edit(); + (EditMode::NotEditing, true, None) + } + KeyCode::Backspace => { + self.edit_buffer.pop(); + (EditMode::AddingNewValue, true, None) + } + KeyCode::Char(c) => { + self.edit_buffer.push(c); + (EditMode::AddingNewValue, true, None) + } + _ => (EditMode::AddingNewValue, false, None), + }, + } + } + + pub fn start_adding_new_value(&mut self, is_secure: bool) { + self.new_key_buffer.clear(); + self.edit_buffer.clear(); + self.is_new_value_secure = is_secure; + self.edit_mode = EditMode::AddingNewKey; + } + + pub fn get_settings(&self) -> &JsonValue { + &self.settings + } + + pub fn get_settings_mut(&mut self) -> &mut JsonValue { + &mut self.settings + } + + pub fn get_current_field(&self) -> usize { + self.current_field + } + + pub fn get_edit_buffer(&self) -> &str { + &self.edit_buffer + } + + pub fn get_new_key_buffer(&self) -> &str { + &self.new_key_buffer + } + + pub fn is_new_value_secure(&self) -> bool { + self.is_new_value_secure + } + + fn move_selection_up(&mut self) { + if self.current_field > 0 { + self.current_field -= 1; + } + } + + fn move_selection_down(&mut self) { + let settings_len = self.settings.as_object().map_or(0, |obj| obj.len()); + if settings_len > 0 && self.current_field < settings_len - 1 { + self.current_field += 1; + } + } + + fn start_editing(&mut self) -> Option { + let current_key = self + .settings + .as_object() + .unwrap() + .keys() + .nth(self.current_field); + + if let Some(key) = current_key { + if !key.starts_with("__") { + let value = &self.settings[key]; + self.edit_buffer = match value { + JsonValue::Object(obj) + if obj.get("was_encrypted") + == Some(&JsonValue::Bool(true)) => + { + match obj.get("content") { + Some(JsonValue::String(s)) => s.clone(), + _ => String::new(), + } + } + JsonValue::Number(n) => n.to_string(), + JsonValue::String(s) => s.clone(), + _ => value.to_string(), + }; + Some(self.edit_buffer.clone()) + } else { + None + } + } else { + None + } + } + + fn confirm_new_key(&mut self) -> bool { + !self.new_key_buffer.is_empty() + } + + fn cancel_edit(&mut self) { + self.edit_buffer.clear(); + self.new_key_buffer.clear(); + self.is_new_value_secure = false; + } + + pub fn get_current_key(&self) -> Option<&str> { + self.settings + .as_object() + .and_then(|obj| obj.keys().nth(self.current_field)) + .map(String::as_str) + } + + pub fn clear(&mut self) { + self.settings = JsonValue::Object(Map::new()); + self.current_field = 0; + self.edit_buffer.clear(); + self.new_key_buffer.clear(); + self.is_new_value_secure = false; + } +} 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 4cfcf4d..9248449 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs @@ -3,9 +3,7 @@ use std::sync::Arc; use lumni::api::error::ApplicationError; use ratatui::widgets::Borders; -use super::modals::{ - ConversationListModal, FileBrowserModal, ProfileEditModal, -}; +use super::modals::{ConversationListModal, FileBrowserModal, SettingsModal}; use super::{ CommandLine, ConversationDatabase, ConversationId, ModalWindowTrait, ModalWindowType, ResponseWindow, TextArea, TextLine, TextWindowTrait, @@ -62,7 +60,7 @@ impl AppUi<'_> { } ModalWindowType::ProfileEdit => { let handler = db_conn.get_profile_handler(None); - Some(Box::new(ProfileEditModal::new(handler).await?)) + Some(Box::new(SettingsModal::new(handler).await?)) } ModalWindowType::FileBrowser => { // TODO: get dir from profile