diff --git a/lumni/src/apps/builtin/llm/prompt/src/app.rs b/lumni/src/apps/builtin/llm/prompt/src/app.rs index 9014300..a55c403 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/app.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/app.rs @@ -25,9 +25,7 @@ use super::chat::{ PromptInstruction, ThreadedChatSession, }; use super::cli::{ - handle_db_subcommand, - handle_profile_subcommand, - parse_cli_arguments, + handle_db_subcommand, handle_profile_subcommand, parse_cli_arguments, }; use super::server::{ModelServer, ServerTrait}; use crate::external as lumni; @@ -45,25 +43,25 @@ async fn create_prompt_instruction( let mut profile_handler = db_conn.get_profile_handler(None); // Handle --profile option -// if let Some(profile_name) = -// matches.and_then(|m| m.get_one::("profile")) -// { -// profile_handler.set_profile_name(profile_name.to_string()); -// } else { -// // Use default profile if set -// if let Some(default_profile) = -// profile_handler.get_default_profile().await? -// { -// profile_handler.set_profile_name(default_profile); -// } -// } -// -// // Check if a profile is set -// if profile_handler.get_profile_name().is_none() { -// return Err(ApplicationError::InvalidInput( -// "No profile set".to_string(), -// )); -// } + // if let Some(profile_name) = + // matches.and_then(|m| m.get_one::("profile")) + // { + // profile_handler.set_profile_name(profile_name.to_string()); + // } else { + // // Use default profile if set + // if let Some(default_profile) = + // profile_handler.get_default_profile().await? + // { + // profile_handler.set_profile_name(default_profile); + // } + // } + // + // // Check if a profile is set + // if profile_handler.get_profile_name().is_none() { + // return Err(ApplicationError::InvalidInput( + // "No profile set".to_string(), + // )); + // } // Get model_backend let model_backend = diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/content_operations.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/content_operations.rs index 46ae825..517f403 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/content_operations.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/content_operations.rs @@ -1,40 +1,198 @@ use lumni::api::error::ApplicationError; +use rusqlite::params; use serde_json::{json, Map, Value as JsonValue}; use super::{ - DatabaseOperationError, EncryptionMode, MaskMode, UserProfileDbHandler, + DatabaseOperationError, EncryptionMode, MaskMode, UserProfile, + UserProfileDbHandler, }; use crate::external as lumni; impl UserProfileDbHandler { - pub fn merge_settings( + pub async fn update_profile_settings( + &mut self, + profile: &UserProfile, + new_settings: &JsonValue, + ) -> Result<(), ApplicationError> { + // Retrieve existing settings and merge with new settings + let existing_settings = + self.get_profile_settings(profile, MaskMode::Unmask).await?; + let merged_settings = + self.merge_settings(&existing_settings, new_settings)?; + + let processed_settings = self.process_settings( + &merged_settings, + EncryptionMode::Encrypt, + MaskMode::Unmask, + )?; + + // Serialize the processed settings + let json_string = + serde_json::to_string(&processed_settings).map_err(|e| { + ApplicationError::InvalidInput(format!( + "Failed to serialize JSON: {}", + e + )) + })?; + + // Update the database + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + let updated_rows = tx + .execute( + "UPDATE user_profiles SET options = ? WHERE id = ?", + params![json_string, profile.id], + ) + .map_err(DatabaseOperationError::SqliteError)?; + + if updated_rows == 0 { + return Err(DatabaseOperationError::ApplicationError( + ApplicationError::InvalidInput(format!( + "Profile with id {} not found", + profile.id + )), + )); + } + + Ok(()) + }) + .map_err(|e| match e { + DatabaseOperationError::SqliteError(sqlite_err) => { + ApplicationError::DatabaseError(sqlite_err.to_string()) + } + DatabaseOperationError::ApplicationError(app_err) => app_err, + })?; + + Ok(()) + } + + pub async fn create_export_json( + &self, + settings: &JsonValue, + ) -> Result { + let (key_path, key_sha256) = self.get_encryption_key_info().await?; + + let export = match settings { + JsonValue::Object(obj) => { + let parameters = obj + .iter() + .map(|(key, value)| { + let processed_value = self + .process_value_with_metadata( + value, + EncryptionMode::Decrypt, + MaskMode::Unmask, + ) + .unwrap_or_else(|_| value.clone()); + + let (param_type, param_value, encrypted) = + if let Some(metadata) = processed_value.as_object() + { + if metadata.get("was_encrypted") + == Some(&JsonValue::Bool(true)) + { + ( + metadata + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"), + metadata + .get("content") + .unwrap_or(value) + .clone(), + true, + ) + } else { + self.get_parameter_info(&processed_value) + } + } else { + self.get_parameter_info(&processed_value) + }; + + json!({ + "Key": key, + "Value": param_value, + "Type": param_type, + "Encrypted": encrypted + }) + }) + .collect::>(); + + json!({ + "Parameters": parameters, + "EncryptionKey": { + "Path": key_path, + "SHA256": key_sha256 + } + }) + } + _ => JsonValue::Null, + }; + Ok(export) + } + + fn get_parameter_info( &self, - current_data: Option, + value: &JsonValue, + ) -> (&'static str, JsonValue, bool) { + match value { + JsonValue::Null => ("Null", value.clone(), false), + JsonValue::Bool(_) => ("Boolean", value.clone(), false), + JsonValue::Number(_) => ("Number", value.clone(), false), + JsonValue::String(_) => ("String", value.clone(), false), + JsonValue::Array(_) => ("Array", value.clone(), false), + JsonValue::Object(_) => ("Object", value.clone(), false), + } + } + + fn get_json_type(&self, value: &JsonValue) -> (&'static str, JsonValue) { + match value { + JsonValue::Null => ("Null", JsonValue::Null), + JsonValue::Bool(_) => ("Boolean", value.clone()), + JsonValue::Number(_) => ("Number", value.clone()), + JsonValue::String(_) => ("String", value.clone()), + JsonValue::Array(_) => ("Array", value.clone()), + JsonValue::Object(_) => ("Object", value.clone()), + } + } + + fn process_value_with_metadata( + &self, + value: &JsonValue, + encryption_mode: EncryptionMode, + mask_mode: MaskMode, + ) -> Result { + if encryption_mode == EncryptionMode::Encrypt { + self.handle_encryption(value) + } else { + let decrypted = self.handle_decryption(value, mask_mode)?; + if Self::is_encrypted_value(value) { + let (param_type, _) = self.get_json_type(&decrypted); + Ok(json!({ + "content": decrypted, + "was_encrypted": true, + "type": param_type + })) + } else { + Ok(decrypted) + } + } + } + + fn merge_settings( + &self, + current_data: &JsonValue, new_settings: &JsonValue, ) -> Result { - if let Some(current_json) = current_data { - let current: JsonValue = serde_json::from_str(¤t_json) - .map_err(|e| { - DatabaseOperationError::ApplicationError( - ApplicationError::InvalidInput(format!( - "Invalid JSON: {}", - e - )), - ) - })?; - - let mut merged = current.clone(); - if let (Some(merged_obj), Some(new_obj)) = - (merged.as_object_mut(), new_settings.as_object()) - { - for (key, new_value) in new_obj { - self.merge_setting(merged_obj, key, new_value, ¤t)?; - } + let mut merged = current_data.clone(); + if let (Some(merged_obj), Some(new_obj)) = + (merged.as_object_mut(), new_settings.as_object()) + { + for (key, new_value) in new_obj { + self.merge_setting(merged_obj, key, new_value, current_data)?; } - Ok(merged) - } else { - Ok(new_settings.clone()) } + Ok(merged) } fn merge_setting( @@ -76,13 +234,26 @@ impl UserProfileDbHandler { new_value: &JsonValue, is_new_value_marked_for_encryption: bool, ) -> Result<(), DatabaseOperationError> { - if is_new_value_marked_for_encryption { - merged_obj.insert(key.clone(), new_value.clone()); - } else if let Some(content) = new_value.as_str() { - let encrypted = self - .encrypt_value(content) + // Check if the existing value is already encrypted + let is_existing_value_encrypted = merged_obj + .get(key) + .and_then(|v| v.as_object()) + .and_then(|obj| obj.get("encryption_key")) + .is_some(); + + if is_new_value_marked_for_encryption || is_existing_value_encrypted { + // If the new value is marked for encryption or the existing value was encrypted, + // we need to encrypt the new value + let encrypted_value = self + .encrypt_value(new_value) .map_err(DatabaseOperationError::ApplicationError)?; - merged_obj.insert(key.clone(), encrypted); + + // Insert the encrypted value + merged_obj.insert(key.clone(), encrypted_value); + } else { + // If it's not marked for encryption and the existing value wasn't encrypted, + // we can insert it as is + merged_obj.insert(key.clone(), new_value.clone()); } Ok(()) } @@ -135,11 +306,27 @@ impl UserProfileDbHandler { value: &JsonValue, ) -> Result { if Self::is_marked_for_encryption(value) { - if let Some(JsonValue::String(content)) = value.get("content") { - self.encrypt_value(content) + if let Some(content) = value.get("content") { + let encrypted_value = self.encrypt_value(content)?; + + // Check if the encrypted_value already has the correct structure + if encrypted_value.is_object() + && encrypted_value.get("content").is_some() + && encrypted_value.get("encryption_key").is_some() + && encrypted_value.get("type_info").is_some() + { + Ok(encrypted_value) + } else { + // If not, construct the correct structure + Ok(json!({ + "content": encrypted_value.get("content").unwrap_or(&JsonValue::Null), + "encryption_key": encrypted_value.get("encryption_key").unwrap_or(&JsonValue::String("".to_string())), + "type_info": encrypted_value.get("type_info").unwrap_or(&JsonValue::String("unknown".to_string())) + })) + } } else { Err(ApplicationError::InvalidInput( - "Invalid secure string format".to_string(), + "Invalid secure value format".to_string(), )) } } else { @@ -203,27 +390,6 @@ impl UserProfileDbHandler { } } - fn process_value_with_metadata( - &self, - value: &JsonValue, - encryption_mode: EncryptionMode, - mask_mode: MaskMode, - ) -> Result { - if encryption_mode == EncryptionMode::Encrypt { - self.handle_encryption(value) - } else { - let decrypted = self.handle_decryption(value, mask_mode)?; - if Self::is_encrypted_value(value) { - Ok(json!({ - "value": decrypted, - "was_encrypted": true - })) - } else { - Ok(decrypted) - } - } - } - pub fn is_encrypted_value(value: &JsonValue) -> bool { if let Some(obj) = value.as_object() { obj.contains_key("content") && obj.contains_key("encryption_key") diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs index 3b8e977..4cd7c3d 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs @@ -10,25 +10,6 @@ use super::{ use crate::external as lumni; impl UserProfileDbHandler { - pub async fn profile_exists( - &self, - profile: &UserProfile, - ) -> Result { - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - let count: i64 = tx - .query_row( - "SELECT COUNT(*) FROM user_profiles WHERE id = ? AND name \ - = ?", - params![profile.id, profile.name], - |row| row.get(0), - ) - .map_err(DatabaseOperationError::SqliteError)?; - Ok(count > 0) - }) - .map_err(ApplicationError::from) - } - pub async fn get_profile_by_id( &self, id: i64, @@ -93,12 +74,17 @@ impl UserProfileDbHandler { .map_err(ApplicationError::from) } - pub async fn list_profiles(&self) -> Result, ApplicationError> { + pub async fn list_profiles( + &self, + ) -> Result, ApplicationError> { let mut db = self.db.lock().await; db.process_queue_with_result(|tx| { let mut stmt = tx - .prepare("SELECT id, name FROM user_profiles ORDER BY created_at DESC") + .prepare( + "SELECT id, name FROM user_profiles ORDER BY created_at \ + DESC", + ) .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; let profiles = stmt .query_map([], |row| { @@ -162,28 +148,6 @@ impl UserProfileDbHandler { .map_err(ApplicationError::from) } - // TODO: should return Vec - //pub async fn get_profile_list( - // &self, - //) -> Result, ApplicationError> { - // let mut db = self.db.lock().await; - // db.process_queue_with_result(|tx| { - // let mut stmt = - // tx.prepare("SELECT name FROM user_profiles ORDER BY name ASC")?; - // let profiles = stmt - // .query_map([], |row| row.get(0))? - // .collect::, _>>() - // .map_err(|e| DatabaseOperationError::SqliteError(e))?; - // Ok(profiles) - // }) - // .map_err(|e| match e { - // DatabaseOperationError::SqliteError(sqlite_err) => { - // ApplicationError::DatabaseError(sqlite_err.to_string()) - // } - // DatabaseOperationError::ApplicationError(app_err) => app_err, - // }) - //} - pub async fn register_encryption_key( &self, name: &str, @@ -269,4 +233,37 @@ impl UserProfileDbHandler { }) .map_err(ApplicationError::from) } + + pub async fn get_encryption_key_info( + &self, + ) -> Result<(String, String), ApplicationError> { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + tx.query_row( + "SELECT encryption_keys.file_path, encryption_keys.sha256_hash + FROM user_profiles + JOIN encryption_keys ON user_profiles.encryption_key_id = \ + encryption_keys.id + WHERE user_profiles.id = ?", + params![self.profile.as_ref().map(|p| p.id).unwrap_or(0)], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(|e| match e { + rusqlite::Error::QueryReturnedNoRows => { + DatabaseOperationError::ApplicationError( + ApplicationError::InvalidUserConfiguration( + "No encryption key found for profile".to_string(), + ), + ) + } + _ => DatabaseOperationError::SqliteError(e), + }) + }) + .map_err(|e| match e { + DatabaseOperationError::SqliteError(sqlite_err) => { + ApplicationError::DatabaseError(sqlite_err.to_string()) + } + DatabaseOperationError::ApplicationError(app_err) => app_err, + }) + } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/encryption_operations.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/encryption_operations.rs index 5977f3b..5cf3ea9 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/encryption_operations.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/encryption_operations.rs @@ -13,11 +13,26 @@ use crate::external as lumni; impl UserProfileDbHandler { pub fn encrypt_value( &self, - content: &str, + content: &JsonValue, ) -> Result { + let type_info = match content { + JsonValue::Null => "null", + JsonValue::Bool(_) => "boolean", + JsonValue::Number(_) => "number", + JsonValue::String(_) => "string", + JsonValue::Array(_) => "array", + JsonValue::Object(_) => "object", + }; + + let content_string = match content { + JsonValue::String(s) => s.clone(), // Use the string value directly + _ => content.to_string(), // For other types, use JSON serialization + }; + if let Some(ref encryption_handler) = self.encryption_handler { - let (encrypted_content, encryption_key) = - encryption_handler.encrypt_string(content).map_err(|e| { + let (encrypted_content, encryption_key) = encryption_handler + .encrypt_string(&content_string) + .map_err(|e| { ApplicationError::EncryptionError( EncryptionError::EncryptionFailed(e.to_string()), ) @@ -25,6 +40,7 @@ impl UserProfileDbHandler { Ok(json!({ "content": encrypted_content, "encryption_key": encryption_key, + "type_info": type_info })) } else { Err(ApplicationError::EncryptionError( @@ -44,22 +60,63 @@ impl UserProfileDbHandler { if let ( Some(JsonValue::String(content)), Some(JsonValue::String(encrypted_key)), - ) = (obj.get("content"), obj.get("encryption_key")) - { - if encrypted_key.is_empty() { - return Ok(JsonValue::String(content.clone())); - } - - encryption_handler + Some(JsonValue::String(type_info)), + ) = ( + obj.get("content"), + obj.get("encryption_key"), + obj.get("type_info"), + ) { + let decrypted_string = encryption_handler .decrypt_string(content, encrypted_key) - .map(JsonValue::String) .map_err(|e| { ApplicationError::EncryptionError( EncryptionError::DecryptionFailed( e.to_string(), ), ) - }) + })?; + + // Parse the decrypted string based on the type_info + let decrypted_value = match type_info.as_str() { + "null" => JsonValue::Null, + "boolean" => JsonValue::Bool( + decrypted_string.parse().map_err(|_| { + ApplicationError::EncryptionError( + EncryptionError::DecryptionFailed( + "Failed to parse boolean".to_string(), + ), + ) + })?, + ), + "number" => serde_json::from_str(&decrypted_string) + .map_err(|_| { + ApplicationError::EncryptionError( + EncryptionError::DecryptionFailed( + "Failed to parse number".to_string(), + ), + ) + })?, + "string" => JsonValue::String(decrypted_string), // Don't parse, use the string directly + "array" | "object" => serde_json::from_str( + &decrypted_string, + ) + .map_err(|_| { + ApplicationError::EncryptionError( + EncryptionError::DecryptionFailed( + "Failed to parse complex type".to_string(), + ), + ) + })?, + _ => { + return Err(ApplicationError::EncryptionError( + EncryptionError::DecryptionFailed( + "Unknown type".to_string(), + ), + )) + } + }; + + Ok(decrypted_value) } else { Err(ApplicationError::EncryptionError( EncryptionError::InvalidKey( 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 87ec1ca..1297aef 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 @@ -64,12 +64,10 @@ impl UserProfileDbHandler { &mut self, ) -> Result, ApplicationError> { // TODO: - return Ok(Some( - ModelBackend { - server: ModelServer::from_str("ollama")?, - model: None, - } - )); + return Ok(Some(ModelBackend { + server: ModelServer::from_str("ollama")?, + model: None, + })); let user_profile = self.profile.clone(); if let Some(profile) = user_profile { @@ -184,42 +182,7 @@ impl UserProfileDbHandler { ) -> Result { let settings = self.get_profile_settings(profile, MaskMode::Unmask).await?; - Ok(self.create_export_json(&settings)) - } - - fn create_export_json(&self, settings: &JsonValue) -> JsonValue { - match settings { - JsonValue::Object(obj) => { - let mut parameters = Vec::new(); - for (key, value) in obj { - let (param_type, param_value) = if let Some(metadata) = - value.as_object() - { - if metadata.get("was_encrypted") - == Some(&JsonValue::Bool(true)) - { - ( - "SecureString", - metadata.get("value").unwrap_or(value).clone(), - ) - } else { - ("String", value.clone()) - } - } else { - ("String", value.clone()) - }; - parameters.push(json!({ - "Key": key, - "Value": param_value, - "Type": param_type - })); - } - json!({ - "Parameters": parameters - }) - } - _ => JsonValue::Null, - } + self.create_export_json(&settings).await } pub async fn truncate_and_vacuum(&self) -> Result<(), ApplicationError> { diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs index 3216f29..d9c51a9 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs @@ -68,37 +68,22 @@ impl UserProfileDbHandler { Ok(profile) } - pub async fn update(&mut self, profile: &UserProfile, new_settings: &JsonValue) -> Result<(), ApplicationError> { - let processed_settings = self.process_settings( - new_settings, - EncryptionMode::Encrypt, - MaskMode::Unmask, - )?; - - let json_string = serde_json::to_string(&processed_settings).map_err(|e| { - ApplicationError::InvalidInput(format!( - "Failed to serialize JSON: {}", - e - )) - })?; + pub async fn update( + &mut self, + profile: &UserProfile, + new_settings: &JsonValue, + ) -> Result<(), ApplicationError> { + // Update profile settings + self.update_profile_settings(profile, new_settings).await?; + // Update the name if it has changed let mut db = self.db.lock().await; db.process_queue_with_result(|tx| { - let updated_rows = tx.execute( - "UPDATE user_profiles SET name = ?, options = ? WHERE id = ?", - params![profile.name, json_string, profile.id], + tx.execute( + "UPDATE user_profiles SET name = ? WHERE id = ?", + params![profile.name, profile.id], ) .map_err(DatabaseOperationError::SqliteError)?; - - if updated_rows == 0 { - return Err(DatabaseOperationError::ApplicationError( - ApplicationError::InvalidInput(format!( - "Profile with id {} not found", - profile.id - )), - )); - } - Ok(()) }) .map_err(|e| match e { @@ -144,7 +129,6 @@ impl UserProfileDbHandler { } } - // TODO: should check based on name and id pub async fn get_profile_settings( &mut self, profile: &UserProfile, diff --git a/lumni/src/apps/builtin/llm/prompt/src/cli/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/cli/mod.rs index 43d91dc..7001cac 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/cli/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/cli/mod.rs @@ -4,9 +4,9 @@ use clap::{Arg, Command}; use lumni::api::spec::ApplicationSpec; use subcommands::db::create_db_subcommand; pub use subcommands::db::handle_db_subcommand; - use subcommands::profile::create_profile_subcommand; pub use subcommands::profile::handle_profile_subcommand; + use super::chat::db::{ ConversationDatabase, EncryptionHandler, MaskMode, ModelSpec, UserProfile, UserProfileDbHandler, diff --git a/lumni/src/apps/builtin/llm/prompt/src/cli/subcommands/profile.rs b/lumni/src/apps/builtin/llm/prompt/src/cli/subcommands/profile.rs index e2d70ae..abb8369 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/cli/subcommands/profile.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/cli/subcommands/profile.rs @@ -2,10 +2,9 @@ use std::path::PathBuf; use clap::{Arg, ArgAction, ArgMatches, Command}; use lumni::api::error::ApplicationError; -use serde_json::{json, Map, Value as JsonValue}; +use serde_json::{json, Map, Number as JsonNumber, Value as JsonValue}; -//use super::profile_helper::interactive_profile_edit; -use super::{MaskMode, UserProfileDbHandler, UserProfile}; +use super::{MaskMode, UserProfile, UserProfileDbHandler}; use crate::external as lumni; pub fn create_profile_subcommand() -> Command { @@ -20,7 +19,6 @@ pub fn create_profile_subcommand() -> Command { .subcommand(create_rm_subcommand()) .subcommand(create_set_default_subcommand()) .subcommand(create_show_default_subcommand()) - //.subcommand(create_edit_subcommand()) .subcommand(create_key_subcommand()) .subcommand(create_export_subcommand()) .subcommand(create_truncate_subcommand()) @@ -45,7 +43,11 @@ fn create_show_subcommand() -> Command { fn create_create_subcommand() -> Command { Command::new("create") .about("Create a new profile") - .arg(Arg::new("name").help("Name of the new profile").required(true)) + .arg( + Arg::new("name") + .help("Name of the new profile") + .required(true), + ) .arg( Arg::new("settings") .long("settings") @@ -60,6 +62,18 @@ fn create_set_subcommand() -> Command { .arg(Arg::new("id").help("ID of the profile")) .arg(Arg::new("key").help("Key to set")) .arg(Arg::new("value").help("Value to set")) + .arg( + Arg::new("type") + .long("type") + .help( + "Specify the type of the value (string, number, boolean, \ + null, array, object)", + ) + .value_parser([ + "string", "number", "boolean", "null", "array", "object", + ]) + .default_value("string"), + ) .arg( Arg::new("secure") .long("secure") @@ -111,18 +125,6 @@ fn create_show_default_subcommand() -> Command { ) } -fn create_edit_subcommand() -> Command { - Command::new("edit") - .about("Add a new profile or edit an existing one with guided setup") - .arg(Arg::new("name").help("Name of the profile to edit (optional)")) - .arg( - Arg::new("ssh-key-path") - .long("ssh-key-path") - .help("Custom SSH key path") - .value_name("PATH"), - ) -} - fn create_key_subcommand() -> Command { Command::new("key") .about("Manage encryption keys for profiles") @@ -175,8 +177,7 @@ fn create_export_subcommand() -> Command { .arg( Arg::new("id") .help( - "ID of the profile to export (omit to export all \ - profiles)", + "ID of the profile to export (omit to export all profiles)", ) .required(false), ) @@ -212,7 +213,10 @@ pub async fn handle_profile_subcommand( println!("Available profiles:"); for profile in profiles { if Some(&profile) == default_profile.as_ref() { - println!(" ID: {} - {} (default)", profile.id, profile.name); + println!( + " ID: {} - {} (default)", + profile.id, profile.name + ); } else { println!(" ID: {} - {}", profile.id, profile.name); } @@ -227,8 +231,13 @@ pub async fn handle_profile_subcommand( MaskMode::Mask }; let profile = get_profile_by_id(&db_handler, id_str).await?; - let settings = db_handler.get_profile_settings(&profile, mask_mode).await?; - println!("Profile ID: {} - {} settings:", profile.id, profile.name); + let settings = db_handler + .get_profile_settings(&profile, mask_mode) + .await?; + println!( + "Profile ID: {} - {} settings:", + profile.id, profile.name + ); for (key, value) in settings.as_object().unwrap() { println!(" {}: {}", key, extract_value(value)); } @@ -239,38 +248,57 @@ pub async fn handle_profile_subcommand( Some(("create", create_matches)) => { let name = create_matches.get_one::("name").unwrap(); - let settings = if let Some(settings_str) = create_matches.get_one::("settings") { - serde_json::from_str(settings_str) - .map_err(|e| ApplicationError::InvalidInput(format!("Invalid JSON for settings: {}", e)))? + let settings = if let Some(settings_str) = + create_matches.get_one::("settings") + { + serde_json::from_str(settings_str).map_err(|e| { + ApplicationError::InvalidInput(format!( + "Invalid JSON for settings: {}", + e + )) + })? } else { JsonValue::Object(Map::new()) }; let new_profile = db_handler.create(name, &settings).await?; - println!("Created new profile - ID: {}, Name: {}", new_profile.id, new_profile.name); + println!( + "Created new profile - ID: {}, Name: {}", + new_profile.id, new_profile.name + ); } Some(("set", set_matches)) => { - if let (Some(id_str), Some(key), Some(value)) = ( + if let (Some(id_str), Some(key), Some(value), Some(type_str)) = ( set_matches.get_one::("id"), set_matches.get_one::("key"), set_matches.get_one::("value"), + set_matches.get_one::("type"), ) { let is_secure = set_matches.get_flag("secure"); let profile = get_profile_by_id(&db_handler, id_str).await?; - let mut settings = JsonValue::Object(Map::new()); + let typed_value = parse_and_validate_value(value, type_str)?; + + let mut settings = JsonValue::Object(serde_json::Map::new()); if is_secure { - settings[key.to_string()] = JsonValue::Object(Map::from_iter(vec![ - ("content".to_string(), JsonValue::String(value.to_string())), - ("encryption_key".to_string(), JsonValue::String("".to_string())), - ])); + settings[key.to_string()] = + JsonValue::Object(serde_json::Map::from_iter(vec![ + ("content".to_string(), typed_value), + ( + "encryption_key".to_string(), + JsonValue::String("".to_string()), + ), + ])); } else { - settings[key.to_string()] = JsonValue::String(value.to_string()); + settings[key.to_string()] = typed_value; } db_handler.update(&profile, &settings).await?; - println!("Profile ID: {} - {} updated. Key '{}' set.", profile.id, profile.name, key); + println!( + "Profile ID: {} - {} updated. Key '{}' set.", + profile.id, profile.name, key + ); } else { create_set_subcommand().print_help()?; } @@ -287,11 +315,16 @@ pub async fn handle_profile_subcommand( MaskMode::Mask }; let profile = get_profile_by_id(&db_handler, id_str).await?; - let settings = db_handler.get_profile_settings(&profile, mask_mode).await?; + let settings = db_handler + .get_profile_settings(&profile, mask_mode) + .await?; if let Some(value) = settings.get(key) { println!("{}: {}", key, extract_value(value)); } else { - println!("Key '{}' not found in profile ID: {} - {}", key, profile.id, profile.name); + println!( + "Key '{}' not found in profile ID: {} - {}", + key, profile.id, profile.name + ); } } else { create_get_subcommand().print_help()?; @@ -308,7 +341,10 @@ pub async fn handle_profile_subcommand( settings[key.to_string()] = JsonValue::Null; // Null indicates deletion db_handler.update(&profile, &settings).await?; - println!("Key '{}' deleted from profile ID: {} - {}.", key, profile.id, profile.name); + println!( + "Key '{}' deleted from profile ID: {} - {}.", + key, profile.id, profile.name + ); } else { create_del_subcommand().print_help()?; } @@ -318,7 +354,10 @@ pub async fn handle_profile_subcommand( if let Some(id_str) = rm_matches.get_one::("id") { let profile = get_profile_by_id(&db_handler, id_str).await?; db_handler.delete_profile(&profile).await?; - println!("Profile ID: {} - {} removed.", profile.id, profile.name); + println!( + "Profile ID: {} - {} removed.", + profile.id, profile.name + ); } else { create_rm_subcommand().print_help()?; } @@ -328,21 +367,32 @@ pub async fn handle_profile_subcommand( if let Some(id_str) = default_matches.get_one::("id") { let profile = get_profile_by_id(&db_handler, id_str).await?; db_handler.set_default_profile(&profile).await?; - println!("Profile ID: {} - {} set as default.", profile.id, profile.name); + println!( + "Profile ID: {} - {} set as default.", + profile.id, profile.name + ); } else { create_set_default_subcommand().print_help()?; } } Some(("show-default", show_default_matches)) => { - if let Some(default_profile) = db_handler.get_default_profile().await? { - println!("Default profile ID: {} - {}", default_profile.id, default_profile.name); - let mask_mode = if show_default_matches.get_flag("show-decrypted") { - MaskMode::Unmask - } else { - MaskMode::Mask - }; - let settings = db_handler.get_profile_settings(&default_profile, mask_mode).await?; + if let Some(default_profile) = + db_handler.get_default_profile().await? + { + println!( + "Default profile ID: {} - {}", + default_profile.id, default_profile.name + ); + let mask_mode = + if show_default_matches.get_flag("show-decrypted") { + MaskMode::Unmask + } else { + MaskMode::Mask + }; + let settings = db_handler + .get_profile_settings(&default_profile, mask_mode) + .await?; println!("Settings:"); for (key, value) in settings.as_object().unwrap() { println!(" {}: {}", key, extract_value(value)); @@ -354,28 +404,33 @@ pub async fn handle_profile_subcommand( Some(("export", export_matches)) => { let output_file = export_matches.get_one::("output"); - let default_profile = db_handler.get_default_profile().await?; - let profiles = if let Some(id_str) = export_matches.get_one::("id") { + let profiles = if let Some(id_str) = + export_matches.get_one::("id") + { // Export a single profile let profile = get_profile_by_id(&db_handler, id_str).await?; - let settings = db_handler.export_profile_settings(&profile).await?; + let settings = + db_handler.export_profile_settings(&profile).await?; vec![json!({ "ID": profile.id, "Name": profile.name, - "Parameters": settings["Parameters"] + "Parameters": settings["Parameters"], + "EncryptionKey": settings["EncryptionKey"] })] } else { // Export all profiles let mut profiles_vec = Vec::new(); let profile_list = db_handler.list_profiles().await?; for profile in profile_list { - let settings = db_handler.export_profile_settings(&profile).await?; + let settings = + db_handler.export_profile_settings(&profile).await?; profiles_vec.push(json!({ "ID": profile.id, "Name": profile.name, - "Parameters": settings["Parameters"] + "Parameters": settings["Parameters"], + "EncryptionKey": settings["EncryptionKey"] })); } profiles_vec @@ -400,19 +455,6 @@ pub async fn handle_profile_subcommand( )?; } - // TODO: should not required anymore -- may be removed - //Some(("edit", edit_matches)) => { - // let profile_name = edit_matches.get_one::("name").cloned(); - // let custom_ssh_key_path = - // edit_matches.get_one::("ssh-key-path").cloned(); - // interactive_profile_edit( - // &mut db_handler, - // profile_name, - // custom_ssh_key_path, - // ) - // .await?; - //} - Some(("key", key_matches)) => match key_matches.subcommand() { Some(("add", add_matches)) => { let name = add_matches.get_one::("name").unwrap(); @@ -487,7 +529,6 @@ fn export_json( success_message: &str, ) -> Result<(), ApplicationError> { let json_string = serde_json::to_string_pretty(json)?; - if let Some(file_path) = output_file { std::fs::write(file_path, json_string)?; println!("{}. Saved to: {}", success_message, file_path); @@ -501,7 +542,7 @@ fn export_json( fn extract_value(value: &JsonValue) -> &JsonValue { if let Some(obj) = value.as_object() { if obj.contains_key("was_encrypted") { - obj.get("value").unwrap_or(value) + obj.get("content").unwrap_or(value) } else { value } @@ -510,12 +551,91 @@ fn extract_value(value: &JsonValue) -> &JsonValue { } } -async fn get_profile_by_id(db_handler: &UserProfileDbHandler, id_str: &str) -> Result { - let id = id_str.parse::().map_err(|_| - ApplicationError::InvalidInput(format!("Invalid profile ID: {}", id_str)))?; - +async fn get_profile_by_id( + db_handler: &UserProfileDbHandler, + id_str: &str, +) -> Result { + let id = id_str.parse::().map_err(|_| { + ApplicationError::InvalidInput(format!( + "Invalid profile ID: {}", + id_str + )) + })?; + match db_handler.get_profile_by_id(id).await? { Some(profile) => Ok(profile), - None => Err(ApplicationError::InvalidInput(format!("No profile found with ID: {}", id))) + None => Err(ApplicationError::InvalidInput(format!( + "No profile found with ID: {}", + id + ))), + } +} + +fn parse_and_validate_value( + value: &str, + type_str: &str, +) -> Result { + match type_str { + "string" => Ok(JsonValue::String(value.to_string())), + "number" => { + // First, try parsing as an integer + if let Ok(int_value) = value.parse::() { + Ok(JsonValue::Number(int_value.into())) + } else { + // If not an integer, try parsing as a float + value + .parse::() + .map_err(|_| { + ApplicationError::InvalidInput(format!( + "Invalid number: {}", + value + )) + }) + .and_then(|float_value| { + JsonNumber::from_f64(float_value) + .map(JsonValue::Number) + .ok_or_else(|| { + ApplicationError::InvalidInput(format!( + "Invalid number: {}", + value + )) + }) + }) + } + } + "boolean" => match value.to_lowercase().as_str() { + "true" => Ok(JsonValue::Bool(true)), + "false" => Ok(JsonValue::Bool(false)), + _ => Err(ApplicationError::InvalidInput(format!( + "Invalid boolean: {}", + value + ))), + }, + "null" => { + if value.to_lowercase() == "null" { + Ok(JsonValue::Null) + } else { + Err(ApplicationError::InvalidInput(format!( + "Invalid null value: {}", + value + ))) + } + } + "array" => serde_json::from_str(value).map_err(|_| { + ApplicationError::InvalidInput(format!( + "Invalid JSON array: {}", + value + )) + }), + "object" => serde_json::from_str(value).map_err(|_| { + ApplicationError::InvalidInput(format!( + "Invalid JSON object: {}", + value + )) + }), + _ => Err(ApplicationError::InvalidInput(format!( + "Invalid type: {}", + type_str + ))), } } 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 bfebfcc..9ef6c4d 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs @@ -22,7 +22,7 @@ pub use window::{ use super::chat::db::{ Conversation, ConversationDatabase, ConversationDbHandler, ConversationId, - ConversationStatus, MaskMode, ModelSpec, UserProfileDbHandler, UserProfile, + ConversationStatus, MaskMode, ModelSpec, 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 702d611..b52abcc 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs @@ -9,9 +9,9 @@ use ratatui::Frame; use super::{ ApplicationError, CommandLine, Conversation, ConversationDbHandler, - ConversationStatus, KeyTrack, MaskMode, ModelServer, ModelSpec, - PromptInstruction, ServerTrait, TextWindowTrait, ThreadedChatSession, - UserEvent, UserProfileDbHandler, UserProfile, WindowEvent, SUPPORTED_MODEL_ENDPOINTS, + ConversationStatus, KeyTrack, MaskMode, ModelServer, PromptInstruction, + ServerTrait, 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 index df4b622..5aa30de 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/mod.rs @@ -21,7 +21,7 @@ use ratatui::widgets::{ }; use ratatui::Frame; use serde_json::{json, Map, Value as JsonValue}; -use settings_editor::SettingsEditor; +use settings_editor::{SettingsAction, SettingsEditor}; use tokio::sync::mpsc; use ui_state::{EditMode, Focus, UIState}; @@ -165,8 +165,7 @@ impl ModalWindowTrait for ProfileEditModal { new_profile.name, new_profile.id ); - self.profile_list - .add_profile(new_profile); + self.profile_list.add_profile(new_profile); self.load_profile().await?; self.ui_state.cancel_new_profile_creation(); @@ -263,156 +262,77 @@ impl ModalWindowTrait for ProfileEditModal { } _ => Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)), }, - Focus::SettingsList => match key_code { - KeyCode::Left - | KeyCode::Char('q') - | KeyCode::Esc - | KeyCode::Tab => { - if self.ui_state.edit_mode == EditMode::NotEditing { - self.ui_state.set_focus(Focus::ProfileList); - } else { - self.cancel_edit(); - } - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - KeyCode::Up => { - self.settings_editor.move_selection_up(); - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - KeyCode::Down => { - self.settings_editor.move_selection_down(); - Ok(WindowEvent::Modal(ModalAction::WaitForKeyEvent)) - } - KeyCode::Enter => { - match self.ui_state.edit_mode { - EditMode::NotEditing => { - if self.settings_editor.start_editing().is_some() { - self.ui_state - .set_edit_mode(EditMode::EditingValue); - } - } - EditMode::EditingValue => { - if let Some(profile) = - self.profile_list.get_selected_profile() - { - self.settings_editor - .save_edit(profile, &mut self.db_handler) - .await?; - } - self.ui_state.set_edit_mode(EditMode::NotEditing); - } - EditMode::AddingNewKey => { - if self.settings_editor.confirm_new_key() { - self.ui_state - .set_edit_mode(EditMode::AddingNewValue); - } - } - EditMode::AddingNewValue => { - if let Some(profile) = - self.profile_list.get_selected_profile() - { - self.settings_editor - .save_new_value( - profile, - &mut self.db_handler, - ) - .await?; + 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(EditMode::NotEditing); - } - _ => {} - } - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - KeyCode::Char('s') | KeyCode::Char('S') => { - self.settings_editor.toggle_secure_visibility(); - 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::Refresh)) - } - KeyCode::Char('n') => { - self.settings_editor.start_adding_new_value(false); - self.ui_state.set_edit_mode(EditMode::AddingNewKey); - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - KeyCode::Char('N') => { - self.settings_editor.start_adding_new_value(true); - self.ui_state.set_edit_mode(EditMode::AddingNewKey); - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - KeyCode::Char('D') => { - if let Some(profile) = - self.profile_list.get_selected_profile() - { - self.settings_editor - .delete_current_key(profile, &mut self.db_handler) - .await?; - } - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - KeyCode::Char('C') => { - if let Some(profile) = - self.profile_list.get_selected_profile() - { - self.settings_editor - .clear_current_key(profile, &mut self.db_handler) - .await?; - } - Ok(WindowEvent::Modal(ModalAction::Refresh)) - } - KeyCode::Char(c) => { - match self.ui_state.edit_mode { - EditMode::EditingValue | EditMode::AddingNewValue => { - let mut current_value = self - .settings_editor - .get_edit_buffer() - .to_string(); - current_value.push(c); - self.settings_editor.set_edit_buffer(current_value); } - EditMode::AddingNewKey => { - let mut current_value = self - .settings_editor - .get_new_key_buffer() - .to_string(); - current_value.push(c); - self.settings_editor - .set_new_key_buffer(current_value); - } - _ => {} } - Ok(WindowEvent::Modal(ModalAction::Refresh)) + self.ui_state.set_edit_mode(new_mode); + return Ok(WindowEvent::Modal(ModalAction::Refresh)); } - KeyCode::Backspace => { - match self.ui_state.edit_mode { - EditMode::EditingValue | EditMode::AddingNewValue => { - let mut current_value = self - .settings_editor - .get_edit_buffer() - .to_string(); - current_value.pop(); - self.settings_editor.set_edit_buffer(current_value); - } - EditMode::AddingNewKey => { - let mut current_value = self - .settings_editor - .get_new_key_buffer() - .to_string(); - current_value.pop(); - self.settings_editor - .set_new_key_buffer(current_value); - } - _ => {} - } - Ok(WindowEvent::Modal(ModalAction::Refresh)) + + // Handle focus change if not handled by SettingsEditor + 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)) + } Focus::NewProfileCreation => { if let Some(creator) = &mut self.ui_state.new_profile_creator { let profile_count = self.profile_list.total_items(); @@ -420,7 +340,7 @@ impl ModalWindowTrait for ProfileEditModal { creator.create_new_profile(profile_count).await?; return Ok(WindowEvent::Modal(ModalAction::Refresh)); } - match creator.handle_input(key_code).await? { + match creator.handle_key_event(key_code).await? { NewProfileCreatorAction::Refresh => { Ok(WindowEvent::Modal(ModalAction::Refresh)) } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/new_profile_creator.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/new_profile_creator.rs index 2713a5f..bd7d20e 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/new_profile_creator.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/profiles/new_profile_creator.rs @@ -102,7 +102,7 @@ impl NewProfileCreator { creator } - pub async fn handle_input( + pub async fn handle_key_event( &mut self, key_code: KeyCode, ) -> Result { @@ -784,14 +784,9 @@ impl NewProfileCreator { tokio::spawn(async move { let result = db_handler - .create( - &new_profile_name_clone, - &json!(settings_clone), - ) - .await; - let _ = tx - .send(BackgroundTaskResult::ProfileCreated(result)) + .create(&new_profile_name_clone, &json!(settings_clone)) .await; + let _ = tx.send(BackgroundTaskResult::ProfileCreated(result)).await; }); self.background_task = Some(rx); 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 index af81972..1bb5340 100644 --- 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 @@ -93,9 +93,9 @@ impl ProfileEditRenderer { .enumerate() .map(|(i, (key, value))| { let is_editable = !key.starts_with("__"); - let is_secure = value.is_object() - && value.get("was_encrypted") - == Some(&serde_json::Value::Bool(true)); + 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 @@ -109,34 +109,9 @@ impl ProfileEditRenderer { profile_edit_modal.settings_editor.get_edit_buffer() ) } else { - let display_value = if is_secure { - if profile_edit_modal.settings_editor.is_show_secure() { - value["value"].as_str().unwrap_or("").to_string() - } else { - "*****".to_string() - } - } else { - value.as_str().unwrap_or("").to_string() - }; - let lock_icon = if is_secure { - if profile_edit_modal.settings_editor.is_show_secure() { - "🔓 " - } else { - "🔒 " - } - } else { - "" - }; - let empty_indicator = if display_value.is_empty() { - " (empty)" - } else { - "" - }; - format!( - "{}{}: {}{}", - lock_icon, key, display_value, empty_indicator - ) + format!("{}: {}", key, display_value) }; + let style = if i == profile_edit_modal.settings_editor.get_current_field() && matches!( 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/profiles/profile_list.rs index 9d7e96c..4e0ec8b 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/profiles/profile_list.rs @@ -7,7 +7,10 @@ pub struct ProfileList { } impl ProfileList { - pub fn new(profiles: Vec, default_profile: Option) -> Self { + pub fn new( + profiles: Vec, + default_profile: Option, + ) -> Self { ProfileList { profiles, selected_index: 0, @@ -72,8 +75,7 @@ impl ProfileList { // TODO: this does not update the database pub fn start_renaming(&self) -> String { - let profile = self.profiles - .get(self.selected_index); + let profile = self.profiles.get(self.selected_index); if let Some(profile) = profile { profile.name.clone() } else { 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 index fe3d0eb..a6ffa4f 100644 --- 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 @@ -1,5 +1,15 @@ 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, @@ -56,10 +66,22 @@ impl SettingsEditor { .nth(self.current_field) .unwrap(); if !current_key.starts_with("__") { - self.edit_buffer = self.settings[current_key] - .as_str() - .unwrap_or("") - .to_string(); + 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 @@ -89,9 +111,57 @@ impl SettingsEditor { .nth(self.current_field) .unwrap() .to_string(); - self.settings[¤t_key] = - JsonValue::String(self.edit_buffer.clone()); - db_handler.update(profile, &self.settings).await + + 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": "", + "type_info": "string", + "was_encrypted": true + }) + } 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( @@ -99,16 +169,53 @@ impl SettingsEditor { profile: &UserProfile, db_handler: &mut UserProfileDbHandler, ) -> Result<(), ApplicationError> { - if self.is_new_value_secure { - self.settings[&self.new_key_buffer] = json!({ - "value": self.edit_buffer, + 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": "", + "type_info": "string", "was_encrypted": true - }); + }) } else { - self.settings[&self.new_key_buffer] = - JsonValue::String(self.edit_buffer.clone()); + 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?; + + // Immediately update the local settings + 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); + } } - db_handler.update(profile, &self.settings).await + + // 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) { @@ -163,8 +270,30 @@ impl SettingsEditor { Ok(()) } - pub fn toggle_secure_visibility(&mut self) { + 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( @@ -179,10 +308,124 @@ impl SettingsEditor { }; 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 @@ -199,16 +442,14 @@ impl SettingsEditor { pub fn is_new_value_secure(&self) -> bool { self.is_new_value_secure } +} - pub fn is_show_secure(&self) -> bool { - self.show_secure - } - - pub fn set_edit_buffer(&mut self, value: String) { - self.edit_buffer = value; - } - - pub fn set_new_key_buffer(&mut self, value: String) { - self.new_key_buffer = value; +fn parse_value(input: &str) -> JsonValue { + if let Ok(num) = input.parse::() { + JsonValue::Number(num.into()) + } else if let Ok(num) = input.parse::() { + JsonValue::Number(serde_json::Number::from_f64(num).unwrap_or(0.into())) + } else { + JsonValue::String(input.to_string()) } }