From 8a607cffe2fabf3084f1028d81aba85f8201ef27 Mon Sep 17 00:00:00 2001 From: Anthony Potappel Date: Sun, 15 Sep 2024 12:07:32 +0200 Subject: [PATCH] refactor user_profile db --- .../db/user_profile/content_operations.rs | 219 ++++------- .../db/user_profile/database_operations.rs | 265 ------------- .../db/user_profile/encryption_operations.rs | 256 +++++++++++- .../prompt/src/chat/db/user_profile/mod.rs | 158 +++----- .../db/user_profile/profile_operations.rs | 227 ----------- .../chat/db/user_profile/provider_config.rs | 21 + .../src/chat/db/user_profile/user_profile.rs | 367 ++++++++++++++++++ .../llm/prompt/src/cli/subcommands/profile.rs | 7 +- .../prompt/src/tui/modals/settings/manager.rs | 51 +-- .../llm/prompt/src/tui/modals/settings/mod.rs | 3 +- .../tui/modals/settings/profile/creator.rs | 5 +- .../llm/prompt/src/tui/widgets/textarea.rs | 5 - 12 files changed, 799 insertions(+), 785 deletions(-) delete mode 100644 lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs delete mode 100644 lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs create mode 100644 lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/user_profile.rs 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 df9c368..aaaf84f 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,69 +1,92 @@ use lumni::api::error::ApplicationError; -use rusqlite::params; use serde_json::{json, Map, Value as JsonValue}; use super::{ - DatabaseOperationError, EncryptionMode, MaskMode, UserProfile, - UserProfileDbHandler, + DatabaseOperationError, EncryptionMode, MaskMode, UserProfileDbHandler, }; use crate::external as lumni; impl UserProfileDbHandler { - 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 - )), - )); + pub fn process_settings( + &self, + value: &JsonValue, + encryption_mode: EncryptionMode, + mask_mode: MaskMode, + ) -> Result { + match value { + JsonValue::Object(obj) => { + let mut new_obj = Map::new(); + for (k, v) in obj { + new_obj.insert( + k.clone(), + self.process_value(v, encryption_mode, mask_mode)?, + ); + } + Ok(JsonValue::Object(new_obj)) + } + JsonValue::Array(arr) => { + let new_arr: Result, _> = arr + .iter() + .map(|v| { + self.process_settings(v, encryption_mode, mask_mode) + }) + .collect(); + Ok(JsonValue::Array(new_arr?)) } + _ => Ok(value.clone()), + } + } - Ok(()) - }) - .map_err(|e| match e { - DatabaseOperationError::SqliteError(sqlite_err) => { - ApplicationError::DatabaseError(sqlite_err.to_string()) + pub fn process_settings_with_metadata( + &self, + value: &JsonValue, + encryption_mode: EncryptionMode, + mask_mode: MaskMode, + ) -> Result { + match value { + JsonValue::Object(obj) => { + let mut new_obj = Map::new(); + for (k, v) in obj { + let processed = self.process_value_with_metadata( + v, + encryption_mode, + mask_mode, + )?; + new_obj.insert(k.clone(), processed); + } + Ok(JsonValue::Object(new_obj)) } - DatabaseOperationError::ApplicationError(app_err) => app_err, - })?; + JsonValue::Array(arr) => { + let new_arr: Result, _> = arr + .iter() + .map(|v| { + self.process_settings_with_metadata( + v, + encryption_mode, + mask_mode, + ) + }) + .collect(); + Ok(JsonValue::Array(new_arr?)) + } + _ => Ok(value.clone()), + } + } - Ok(()) + pub fn merge_settings( + &self, + current_data: &JsonValue, + new_settings: &JsonValue, + ) -> Result { + 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) } pub async fn create_export_json( @@ -179,22 +202,6 @@ impl UserProfileDbHandler { } } - fn merge_settings( - &self, - current_data: &JsonValue, - new_settings: &JsonValue, - ) -> Result { - 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) - } - fn merge_setting( &self, merged_obj: &mut Map, @@ -258,37 +265,7 @@ impl UserProfileDbHandler { Ok(()) } - pub fn process_settings( - &self, - value: &JsonValue, - encryption_mode: EncryptionMode, - mask_mode: MaskMode, - ) -> Result { - match value { - JsonValue::Object(obj) => { - let mut new_obj = Map::new(); - for (k, v) in obj { - new_obj.insert( - k.clone(), - self.process_value(v, encryption_mode, mask_mode)?, - ); - } - Ok(JsonValue::Object(new_obj)) - } - JsonValue::Array(arr) => { - let new_arr: Result, _> = arr - .iter() - .map(|v| { - self.process_settings(v, encryption_mode, mask_mode) - }) - .collect(); - Ok(JsonValue::Array(new_arr?)) - } - _ => Ok(value.clone()), - } - } - - pub fn process_value( + fn process_value( &self, value: &JsonValue, encryption_mode: EncryptionMode, @@ -354,43 +331,7 @@ impl UserProfileDbHandler { } } - pub fn process_settings_with_metadata( - &self, - value: &JsonValue, - encryption_mode: EncryptionMode, - mask_mode: MaskMode, - ) -> Result { - match value { - JsonValue::Object(obj) => { - let mut new_obj = Map::new(); - for (k, v) in obj { - let processed = self.process_value_with_metadata( - v, - encryption_mode, - mask_mode, - )?; - new_obj.insert(k.clone(), processed); - } - Ok(JsonValue::Object(new_obj)) - } - JsonValue::Array(arr) => { - let new_arr: Result, _> = arr - .iter() - .map(|v| { - self.process_settings_with_metadata( - v, - encryption_mode, - mask_mode, - ) - }) - .collect(); - Ok(JsonValue::Array(new_arr?)) - } - _ => Ok(value.clone()), - } - } - - pub fn is_encrypted_value(value: &JsonValue) -> bool { + fn is_encrypted_value(value: &JsonValue) -> bool { if let Some(obj) = value.as_object() { obj.contains_key("content") && obj.contains_key("encryption_key") } else { @@ -398,7 +339,7 @@ impl UserProfileDbHandler { } } - pub fn is_marked_for_encryption(value: &JsonValue) -> bool { + fn is_marked_for_encryption(value: &JsonValue) -> bool { if let Some(obj) = value.as_object() { obj.contains_key("content") && obj.get("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 deleted file mode 100644 index 4722088..0000000 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/database_operations.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::path::PathBuf; - -use lumni::api::error::ApplicationError; -use rusqlite::{params, OptionalExtension}; - -use super::{ - DatabaseOperationError, EncryptionHandler, UserProfile, - UserProfileDbHandler, -}; -use crate::external as lumni; - -impl UserProfileDbHandler { - pub async fn get_profile_by_id( - &self, - id: i64, - ) -> Result, ApplicationError> { - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - tx.query_row( - "SELECT id, name FROM user_profiles WHERE id = ?", - params![id], - |row| { - Ok(UserProfile { - id: row.get(0)?, - name: row.get(1)?, - }) - }, - ) - .optional() - .map_err(|e| DatabaseOperationError::SqliteError(e)) - }) - .map_err(ApplicationError::from) - } - - pub async fn get_profiles_by_name( - &self, - name: &str, - ) -> 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 WHERE name = ?") - .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; - let profiles = stmt - .query_map(params![name], |row| { - Ok(UserProfile { - id: row.get(0)?, - name: row.get(1)?, - }) - }) - .map_err(|e| ApplicationError::DatabaseError(e.to_string()))? - .collect::, _>>() - .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; - Ok(profiles) - }) - .map_err(ApplicationError::from) - } - - pub async fn delete_profile( - &self, - profile: &UserProfile, - ) -> Result<(), ApplicationError> { - let mut db = self.db.lock().await; - - db.process_queue_with_result(|tx| { - tx.execute( - "DELETE FROM user_profiles WHERE id = ? AND name = ?", - params![profile.id, profile.name], - ) - .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; - Ok(()) - }) - .map_err(ApplicationError::from) - } - - 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", - ) - .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; - let profiles = stmt - .query_map([], |row| { - Ok(UserProfile { - id: row.get(0)?, - name: row.get(1)?, - }) - }) - .map_err(|e| ApplicationError::DatabaseError(e.to_string()))? - .collect::, _>>() - .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; - Ok(profiles) - }) - .map_err(ApplicationError::from) - } - - pub async fn get_default_profile( - &self, - ) -> Result, ApplicationError> { - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - tx.query_row( - "SELECT id, name FROM user_profiles WHERE is_default = 1", - [], - |row| { - Ok(UserProfile { - id: row.get(0)?, - name: row.get(1)?, - }) - }, - ) - .optional() - .map_err(|e| DatabaseOperationError::SqliteError(e)) - }) - .map_err(ApplicationError::from) - } - - pub async fn set_default_profile( - &self, - profile: &UserProfile, - ) -> Result<(), ApplicationError> { - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - tx.execute( - "UPDATE user_profiles SET is_default = 0 WHERE is_default = 1", - [], - ) - .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; - tx.execute( - "UPDATE user_profiles SET is_default = 1 WHERE id = ? AND \ - name = ?", - params![profile.id, profile.name], - ) - .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; - Ok(()) - }) - .map_err(ApplicationError::from) - } - - pub async fn register_encryption_key( - &self, - name: &str, - file_path: &PathBuf, - ) -> Result<(), ApplicationError> { - let hash = EncryptionHandler::get_private_key_hash(file_path)?; - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - tx.execute( - "INSERT INTO encryption_keys (name, file_path, sha256_hash) \ - VALUES (?, ?, ?)", - params![name, file_path.to_str().unwrap(), hash], - ) - .map_err(|e| DatabaseOperationError::SqliteError(e))?; - Ok(()) - }) - .map_err(ApplicationError::from) - } - - pub async fn get_encryption_key( - &self, - name: &str, - ) -> Result<(String, String), ApplicationError> { - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - tx.query_row( - "SELECT file_path, sha256_hash FROM encryption_keys WHERE \ - name = ?", - params![name], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .map_err(|e| DatabaseOperationError::SqliteError(e)) - }) - .map_err(ApplicationError::from) - } - - pub async fn remove_encryption_key( - &self, - name: &str, - ) -> Result<(), ApplicationError> { - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - tx.execute( - "DELETE FROM encryption_keys WHERE name = ?", - params![name], - ) - .map_err(|e| DatabaseOperationError::SqliteError(e))?; - Ok(()) - }) - .map_err(ApplicationError::from) - } - - pub async fn list_encryption_keys( - &self, - key_type: Option<&str>, - ) -> Result, ApplicationError> { - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - let query = match key_type { - Some(_) => { - "SELECT name FROM encryption_keys WHERE key_type = ?" - } - None => "SELECT name FROM encryption_keys", - }; - - let mut stmt = tx - .prepare(query) - .map_err(|e| DatabaseOperationError::SqliteError(e))?; - - let row_mapper = |row: &rusqlite::Row| row.get(0); - - let rows = match key_type { - Some(ktype) => stmt.query_map(params![ktype], row_mapper), - None => stmt.query_map([], row_mapper), - } - .map_err(|e| DatabaseOperationError::SqliteError(e))?; - - let keys = rows - .collect::, _>>() - .map_err(|e| DatabaseOperationError::SqliteError(e))?; - - Ok(keys) - }) - .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 3ece916..dab3352 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 @@ -1,8 +1,10 @@ use std::fs; +use std::path::PathBuf; use std::sync::Arc; use dirs::home_dir; use lumni::api::error::{ApplicationError, EncryptionError}; +use rusqlite::{params, OptionalExtension, Transaction}; use serde_json::{json, Value as JsonValue}; use sha2::{Digest, Sha256}; @@ -156,24 +158,72 @@ impl UserProfileDbHandler { Ok(()) } - fn calculate_current_key_hash(&self) -> Result { - if let Some(ref encryption_handler) = self.encryption_handler { - let key_data = - encryption_handler.get_private_key_pem().map_err(|e| { - ApplicationError::EncryptionError( - EncryptionError::InvalidKey(e.to_string()), - ) - })?; - let mut hasher = Sha256::new(); - hasher.update(key_data.as_bytes()); - Ok(format!("{:x}", hasher.finalize())) - } else { - Err(ApplicationError::EncryptionError( - EncryptionError::InvalidKey( - "Encryption handler required to validate hash".to_string(), - ), - )) - } + pub fn set_encryption_handler( + &mut self, + encryption_handler: Arc, + ) -> Result<(), ApplicationError> { + // If profile is not yet set, return error as we need to know the profile to validate against existing encryption handler + let profile = self.profile.as_ref().ok_or_else(|| { + ApplicationError::InvalidInput( + "UserProfile must be defined before setting encryption handler" + .to_string(), + ) + })?; + + // Check if the profile exists in the database and compare encryption handlers + let db = self.db.clone(); + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let mut db = db.lock().await; + db.process_queue_with_result(|tx| { + let existing_key: Option<(String, String)> = 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.name = ?", + params![profile.name], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(DatabaseOperationError::SqliteError)?; + + if let Some((_, existing_hash)) = existing_key { + let new_path = encryption_handler.get_key_path(); + let new_hash = + EncryptionHandler::get_private_key_hash(&new_path)?; + + if existing_hash != new_hash { + return Err( + DatabaseOperationError::ApplicationError( + ApplicationError::InvalidInput( + "New encryption handler does not \ + match the existing one for this \ + profile" + .to_string(), + ), + ), + ); + } + } + Ok(()) + }) + }) + }); + + result.map_err(|e| match e { + DatabaseOperationError::SqliteError(sqlite_err) => { + ApplicationError::DatabaseError(sqlite_err.to_string()) + } + DatabaseOperationError::ApplicationError(app_err) => app_err, + })?; + + // If we've made it this far, either the profile doesn't exist yet or the encryption handler matches + self.encryption_handler = Some(encryption_handler); + Ok(()) } pub async fn get_or_create_encryption_key( @@ -237,4 +287,174 @@ impl UserProfileDbHandler { } Ok(key_id) } + + pub async fn register_encryption_key( + &self, + name: &str, + file_path: &PathBuf, + ) -> Result<(), ApplicationError> { + let hash = EncryptionHandler::get_private_key_hash(file_path)?; + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + tx.execute( + "INSERT INTO encryption_keys (name, file_path, sha256_hash) \ + VALUES (?, ?, ?)", + params![name, file_path.to_str().unwrap(), hash], + ) + .map_err(|e| DatabaseOperationError::SqliteError(e))?; + Ok(()) + }) + .map_err(ApplicationError::from) + } + + pub async fn get_encryption_key( + &self, + name: &str, + ) -> Result<(String, String), ApplicationError> { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + tx.query_row( + "SELECT file_path, sha256_hash FROM encryption_keys WHERE \ + name = ?", + params![name], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(|e| DatabaseOperationError::SqliteError(e)) + }) + .map_err(ApplicationError::from) + } + + pub async fn remove_encryption_key( + &self, + name: &str, + ) -> Result<(), ApplicationError> { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + tx.execute( + "DELETE FROM encryption_keys WHERE name = ?", + params![name], + ) + .map_err(|e| DatabaseOperationError::SqliteError(e))?; + Ok(()) + }) + .map_err(ApplicationError::from) + } + + pub async fn list_encryption_keys( + &self, + key_type: Option<&str>, + ) -> Result, ApplicationError> { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + let query = match key_type { + Some(_) => { + "SELECT name FROM encryption_keys WHERE key_type = ?" + } + None => "SELECT name FROM encryption_keys", + }; + + let mut stmt = tx + .prepare(query) + .map_err(|e| DatabaseOperationError::SqliteError(e))?; + + let row_mapper = |row: &rusqlite::Row| row.get(0); + + let rows = match key_type { + Some(ktype) => stmt.query_map(params![ktype], row_mapper), + None => stmt.query_map([], row_mapper), + } + .map_err(|e| DatabaseOperationError::SqliteError(e))?; + + let keys = rows + .collect::, _>>() + .map_err(|e| DatabaseOperationError::SqliteError(e))?; + + Ok(keys) + }) + .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, + }) + } + + fn calculate_current_key_hash(&self) -> Result { + if let Some(ref encryption_handler) = self.encryption_handler { + let key_data = + encryption_handler.get_private_key_pem().map_err(|e| { + ApplicationError::EncryptionError( + EncryptionError::InvalidKey(e.to_string()), + ) + })?; + let mut hasher = Sha256::new(); + hasher.update(key_data.as_bytes()); + Ok(format!("{:x}", hasher.finalize())) + } else { + Err(ApplicationError::EncryptionError( + EncryptionError::InvalidKey( + "Encryption handler required to validate hash".to_string(), + ), + )) + } + } + + fn get_or_insert_encryption_key<'a>( + &self, + tx: &Transaction<'a>, + key_path: &PathBuf, + key_hash: &str, + ) -> Result { + // First, try to find an existing key with the same hash + let existing_key_id: Option = tx + .query_row( + "SELECT id FROM encryption_keys WHERE sha256_hash = ?", + params![key_hash], + |row| row.get(0), + ) + .optional() + .map_err(DatabaseOperationError::SqliteError)?; + + match existing_key_id { + Some(id) => Ok(id), + None => { + // If no existing key found, insert a new one + tx.query_row( + "INSERT INTO encryption_keys (file_path, sha256_hash) \ + VALUES (?, ?) RETURNING id", + params![key_path.to_str().unwrap(), key_hash], + |row| row.get(0), + ) + .map_err(DatabaseOperationError::SqliteError) + } + } + } } 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 c72170c..4614300 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 @@ -1,14 +1,13 @@ mod content_operations; -mod database_operations; mod encryption_operations; -mod profile_operations; mod provider_config; - -use std::collections::HashMap; +mod user_profile; +use std::path::PathBuf; use std::sync::Arc; use lumni::api::error::{ApplicationError, EncryptionError}; -use rusqlite::{params, OptionalExtension}; +pub use provider_config::{ProviderConfig, ProviderConfigOptions}; +use rusqlite::params; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use tokio::sync::Mutex as TokioMutex; @@ -18,12 +17,6 @@ use super::encryption::EncryptionHandler; use super::{ModelBackend, ModelServer, ModelSpec}; use crate::external as lumni; -#[derive(Debug, Clone, PartialEq)] -pub struct UserProfile { - pub id: i64, - pub name: String, -} - #[derive(Debug, Clone)] pub struct UserProfileDbHandler { pub profile: Option, @@ -31,22 +24,10 @@ 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 { +#[derive(Debug, Clone, PartialEq)] +pub struct UserProfile { + pub id: i64, pub name: String, - pub display_name: String, - pub value: String, - pub is_secure: bool, - pub placeholder: String, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -78,16 +59,13 @@ impl UserProfileDbHandler { self.profile = Some(profile); } - pub fn get_profile(&self) -> Option<&UserProfile> { - self.profile.as_ref() - } - pub async fn model_backend( &mut self, ) -> Result, ApplicationError> { let user_profile = self.profile.clone(); if let Some(profile) = user_profile { + self.unlock_profile_settings(&profile).await?; let settings = self .get_profile_settings(&profile, MaskMode::Unmask) .await?; @@ -114,88 +92,68 @@ impl UserProfileDbHandler { } } - pub fn set_encryption_handler( + pub async fn unlock_profile_settings( &mut self, - encryption_handler: Arc, + profile: &UserProfile, ) -> Result<(), ApplicationError> { - // If profile is not yet set, return error as we need to know the profile to validate against existing encryption handler - let profile = self.profile.as_ref().ok_or_else(|| { - ApplicationError::InvalidInput( - "UserProfile must be defined before setting encryption handler" - .to_string(), - ) - })?; + // check if profile is already set + if let Some(current_profile) = &self.profile { + if current_profile == profile && self.encryption_handler.is_some() { + // profile already set + return Ok(()); + } + } + // change profile + self.profile = Some(profile.clone()); + self.unlock_current_profile().await + } - // Check if the profile exists in the database and compare encryption handlers - let db = self.db.clone(); - let result = tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async { - let mut db = db.lock().await; - db.process_queue_with_result(|tx| { - let existing_key: Option<(String, String)> = 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.name = ?", - params![profile.name], - |row| Ok((row.get(0)?, row.get(1)?)), + async fn unlock_current_profile(&mut self) -> Result<(), ApplicationError> { + let profile = if self.profile.is_none() { + return Err(ApplicationError::InvalidInput( + "Profile not set".to_string(), + )); + } else { + self.profile.clone().unwrap() + }; + + let (key_hash, key_path): (String, String) = { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + tx.query_row( + "SELECT encryption_keys.sha256_hash, \ + encryption_keys.file_path + FROM user_profiles + JOIN encryption_keys ON user_profiles.encryption_key_id = \ + encryption_keys.id + WHERE user_profiles.name = ?", + params![profile.name], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(DatabaseOperationError::SqliteError) + })? + }; + if self.encryption_handler.is_none() { + // encryption handled required for decryption + let encryption_handler = + EncryptionHandler::new_from_path(&PathBuf::from(&key_path))? + .ok_or_else(|| { + ApplicationError::InvalidInput( + "Failed to load encryption handler".to_string(), ) - .optional() - .map_err(DatabaseOperationError::SqliteError)?; - - if let Some((_, existing_hash)) = existing_key { - let new_path = encryption_handler.get_key_path(); - let new_hash = - EncryptionHandler::get_private_key_hash(&new_path)?; - - if existing_hash != new_hash { - return Err( - DatabaseOperationError::ApplicationError( - ApplicationError::InvalidInput( - "New encryption handler does not \ - match the existing one for this \ - profile" - .to_string(), - ), - ), - ); - } - } - - Ok(()) - }) - }) - }); - - result.map_err(|e| match e { - DatabaseOperationError::SqliteError(sqlite_err) => { - ApplicationError::DatabaseError(sqlite_err.to_string()) - } - DatabaseOperationError::ApplicationError(app_err) => app_err, - })?; + })?; - // If we've made it this far, either the profile doesn't exist yet or the encryption handler matches - self.encryption_handler = Some(encryption_handler); + self.set_encryption_handler(Arc::new(encryption_handler))?; + self.verify_encryption_key_hash(&key_hash)?; + } Ok(()) } - pub fn set_profile_with_encryption_handler( - &mut self, - profile: UserProfile, - encryption_handler: Arc, - ) -> Result<(), ApplicationError> { - self.set_profile(profile); - self.set_encryption_handler(encryption_handler) - } - pub async fn export_profile_settings( &mut self, profile: &UserProfile, ) -> Result { + self.unlock_profile_settings(profile).await?; let settings = self.get_profile_settings(profile, MaskMode::Unmask).await?; self.create_export_json(&settings).await 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 deleted file mode 100644 index 6e0e970..0000000 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/profile_operations.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use lumni::api::error::ApplicationError; -use lumni::Timestamp; -use rusqlite::{params, OptionalExtension, Transaction}; -use serde_json::Value as JsonValue; -use tokio::time::{sleep, Duration}; - -use super::{ - DatabaseOperationError, EncryptionHandler, EncryptionMode, MaskMode, - UserProfile, UserProfileDbHandler, -}; -use crate::external as lumni; - -impl UserProfileDbHandler { - pub async fn create( - &mut self, - profile_name: &str, - settings: &JsonValue, - ) -> Result { - // Simulate a 3-second delay - // sleep(Duration::from_secs(3)).await; - let timestamp = Timestamp::from_system_time().unwrap().as_millis(); - - let encryption_key_id = self.get_or_create_encryption_key().await?; - let processed_settings = self.process_settings( - 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 - )) - })?; - - let mut db = self.db.lock().await; - let profile = db - .process_queue_with_result(|tx| { - tx.execute( - "INSERT INTO user_profiles (name, options, \ - encryption_key_id, created_at) VALUES (?, ?, ?, ?)", - params![ - profile_name, - json_string, - encryption_key_id, - timestamp - ], - ) - .map_err(DatabaseOperationError::SqliteError)?; - - let id = tx.last_insert_rowid(); - Ok(UserProfile { - id, - name: profile_name.to_string(), - }) - }) - .map_err(|e| match e { - DatabaseOperationError::SqliteError(sqlite_err) => { - ApplicationError::DatabaseError(sqlite_err.to_string()) - } - DatabaseOperationError::ApplicationError(app_err) => app_err, - })?; - - Ok(profile) - } - - 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| { - tx.execute( - "UPDATE user_profiles SET name = ? WHERE id = ?", - params![profile.name, profile.id], - ) - .map_err(DatabaseOperationError::SqliteError)?; - Ok(()) - }) - .map_err(|e| match e { - DatabaseOperationError::SqliteError(sqlite_err) => { - ApplicationError::DatabaseError(sqlite_err.to_string()) - } - DatabaseOperationError::ApplicationError(app_err) => app_err, - })?; - - self.profile = Some(profile.clone()); - - Ok(()) - } - - pub fn get_or_insert_encryption_key<'a>( - &self, - tx: &Transaction<'a>, - key_path: &PathBuf, - key_hash: &str, - ) -> Result { - // First, try to find an existing key with the same hash - let existing_key_id: Option = tx - .query_row( - "SELECT id FROM encryption_keys WHERE sha256_hash = ?", - params![key_hash], - |row| row.get(0), - ) - .optional() - .map_err(DatabaseOperationError::SqliteError)?; - - match existing_key_id { - Some(id) => Ok(id), - None => { - // If no existing key found, insert a new one - tx.query_row( - "INSERT INTO encryption_keys (file_path, sha256_hash) \ - VALUES (?, ?) RETURNING id", - params![key_path.to_str().unwrap(), key_hash], - |row| row.get(0), - ) - .map_err(DatabaseOperationError::SqliteError) - } - } - } - - pub async fn get_profile_settings( - &mut self, - profile: &UserProfile, - mask_mode: MaskMode, - ) -> Result { - log::debug!( - "Getting settings for profile: {}:{} ({:?})", - profile.id, - profile.name, - mask_mode - ); - let (json_string, key_hash, key_path): (String, String, String) = { - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - tx.query_row( - "SELECT user_profiles.options, \ - encryption_keys.sha256_hash, encryption_keys.file_path - FROM user_profiles - JOIN encryption_keys ON user_profiles.encryption_key_id = \ - encryption_keys.id - WHERE user_profiles.name = ?", - params![profile.name], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), - ) - .map_err(DatabaseOperationError::SqliteError) - })? - }; - if mask_mode == MaskMode::Unmask && self.encryption_handler.is_none() { - // encryption handled required for decryption - let encryption_handler = - EncryptionHandler::new_from_path(&PathBuf::from(&key_path))? - .ok_or_else(|| { - ApplicationError::InvalidInput( - "Failed to load encryption handler".to_string(), - ) - })?; - self.set_profile_with_encryption_handler( - profile.clone(), - Arc::new(encryption_handler), - )?; - self.verify_encryption_key_hash(&key_hash)?; - } - let settings: JsonValue = - serde_json::from_str(&json_string).map_err(|e| { - ApplicationError::InvalidInput(format!("Invalid JSON: {}", e)) - })?; - self.process_settings_with_metadata( - &settings, - EncryptionMode::Decrypt, - mask_mode, - ) - } - - pub async fn rename_profile( - &self, - profile: &UserProfile, - new_name: &str, - ) -> Result<(), ApplicationError> { - log::debug!( - "Renaming profile '{}' (ID: {}) to '{}'", - profile.name, - profile.id, - new_name - ); - if profile.name == new_name { - return Ok(()); // No need to rename if the names are the same - } - - let mut db = self.db.lock().await; - db.process_queue_with_result(|tx| { - // Perform the rename - let updated_rows = tx.execute( - "UPDATE user_profiles SET name = ? WHERE id = ?", - params![new_name, profile.id], - )?; - - if updated_rows == 0 { - Err(DatabaseOperationError::ApplicationError( - ApplicationError::InvalidInput(format!( - "Profile '{}' (ID: {}) not found", - profile.name, profile.id - )), - )) - } else { - Ok(()) - } - }) - .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/provider_config.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/provider_config.rs index 2cdd4c1..363e4f9 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 @@ -1,7 +1,28 @@ +use std::collections::HashMap; use std::path::PathBuf; +use rusqlite::params; + 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 ProviderConfigOptions { + pub name: String, + pub display_name: String, + pub value: String, + pub is_secure: bool, + pub placeholder: String, +} + impl UserProfileDbHandler { pub async fn save_provider_config( &mut self, diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/user_profile.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/user_profile.rs new file mode 100644 index 0000000..8885887 --- /dev/null +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/user_profile.rs @@ -0,0 +1,367 @@ +use lumni::api::error::ApplicationError; +use lumni::Timestamp; +use rusqlite::{params, OptionalExtension}; +use serde_json::Value as JsonValue; + +use super::{ + DatabaseOperationError, EncryptionMode, MaskMode, UserProfile, + UserProfileDbHandler, +}; +use crate::external as lumni; + +impl UserProfileDbHandler { + pub async fn create_profile( + &mut self, + profile_name: &str, + settings: &JsonValue, + ) -> Result { + let timestamp = Timestamp::from_system_time().unwrap().as_millis(); + + let encryption_key_id = self.get_or_create_encryption_key().await?; + let processed_settings = self.process_settings( + 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 + )) + })?; + + let mut db = self.db.lock().await; + let profile = db + .process_queue_with_result(|tx| { + tx.execute( + "INSERT INTO user_profiles (name, options, \ + encryption_key_id, created_at) VALUES (?, ?, ?, ?)", + params![ + profile_name, + json_string, + encryption_key_id, + timestamp + ], + ) + .map_err(DatabaseOperationError::SqliteError)?; + + let id = tx.last_insert_rowid(); + Ok(UserProfile { + id, + name: profile_name.to_string(), + }) + }) + .map_err(|e| match e { + DatabaseOperationError::SqliteError(sqlite_err) => { + ApplicationError::DatabaseError(sqlite_err.to_string()) + } + DatabaseOperationError::ApplicationError(app_err) => app_err, + })?; + + Ok(profile) + } + + pub async fn update_profile( + &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| { + tx.execute( + "UPDATE user_profiles SET name = ? WHERE id = ?", + params![profile.name, profile.id], + ) + .map_err(DatabaseOperationError::SqliteError)?; + Ok(()) + }) + .map_err(|e| match e { + DatabaseOperationError::SqliteError(sqlite_err) => { + ApplicationError::DatabaseError(sqlite_err.to_string()) + } + DatabaseOperationError::ApplicationError(app_err) => app_err, + })?; + + self.profile = Some(profile.clone()); + + Ok(()) + } + + pub async fn get_profile_settings( + &mut self, + profile: &UserProfile, + mask_mode: MaskMode, + ) -> Result { + log::debug!( + "Getting settings for profile: {}:{} ({:?})", + profile.id, + profile.name, + mask_mode + ); + let json_string: String = { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + tx.query_row( + "SELECT user_profiles.options FROM user_profiles + WHERE user_profiles.name = ?", + params![profile.name], + |row| Ok((row.get(0)?)), + ) + .map_err(DatabaseOperationError::SqliteError) + })? + }; + if mask_mode == MaskMode::Unmask && self.encryption_handler.is_none() { + return Err(ApplicationError::InvalidInput( + "Encryption handler not set".to_string(), + )); + } + let settings: JsonValue = + serde_json::from_str(&json_string).map_err(|e| { + ApplicationError::InvalidInput(format!("Invalid JSON: {}", e)) + })?; + self.process_settings_with_metadata( + &settings, + EncryptionMode::Decrypt, + mask_mode, + ) + } + + pub async fn rename_profile( + &self, + profile: &UserProfile, + new_name: &str, + ) -> Result<(), ApplicationError> { + log::debug!( + "Renaming profile '{}' (ID: {}) to '{}'", + profile.name, + profile.id, + new_name + ); + if profile.name == new_name { + return Ok(()); // No need to rename if the names are the same + } + + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + // Perform the rename + let updated_rows = tx.execute( + "UPDATE user_profiles SET name = ? WHERE id = ?", + params![new_name, profile.id], + )?; + + if updated_rows == 0 { + Err(DatabaseOperationError::ApplicationError( + ApplicationError::InvalidInput(format!( + "Profile '{}' (ID: {}) not found", + profile.name, profile.id + )), + )) + } else { + Ok(()) + } + }) + .map_err(|e| match e { + DatabaseOperationError::SqliteError(sqlite_err) => { + ApplicationError::DatabaseError(sqlite_err.to_string()) + } + DatabaseOperationError::ApplicationError(app_err) => app_err, + }) + } + + 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 get_profile_by_id( + &self, + id: i64, + ) -> Result, ApplicationError> { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + tx.query_row( + "SELECT id, name FROM user_profiles WHERE id = ?", + params![id], + |row| { + Ok(UserProfile { + id: row.get(0)?, + name: row.get(1)?, + }) + }, + ) + .optional() + .map_err(|e| DatabaseOperationError::SqliteError(e)) + }) + .map_err(ApplicationError::from) + } + + pub async fn get_profiles_by_name( + &self, + name: &str, + ) -> 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 WHERE name = ?") + .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; + let profiles = stmt + .query_map(params![name], |row| { + Ok(UserProfile { + id: row.get(0)?, + name: row.get(1)?, + }) + }) + .map_err(|e| ApplicationError::DatabaseError(e.to_string()))? + .collect::, _>>() + .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; + Ok(profiles) + }) + .map_err(ApplicationError::from) + } + + pub async fn delete_profile( + &self, + profile: &UserProfile, + ) -> Result<(), ApplicationError> { + let mut db = self.db.lock().await; + + db.process_queue_with_result(|tx| { + tx.execute( + "DELETE FROM user_profiles WHERE id = ? AND name = ?", + params![profile.id, profile.name], + ) + .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; + Ok(()) + }) + .map_err(ApplicationError::from) + } + + 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", + ) + .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; + let profiles = stmt + .query_map([], |row| { + Ok(UserProfile { + id: row.get(0)?, + name: row.get(1)?, + }) + }) + .map_err(|e| ApplicationError::DatabaseError(e.to_string()))? + .collect::, _>>() + .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; + Ok(profiles) + }) + .map_err(ApplicationError::from) + } + + pub async fn get_default_profile( + &self, + ) -> Result, ApplicationError> { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + tx.query_row( + "SELECT id, name FROM user_profiles WHERE is_default = 1", + [], + |row| { + Ok(UserProfile { + id: row.get(0)?, + name: row.get(1)?, + }) + }, + ) + .optional() + .map_err(|e| DatabaseOperationError::SqliteError(e)) + }) + .map_err(ApplicationError::from) + } + + pub async fn set_default_profile( + &self, + profile: &UserProfile, + ) -> Result<(), ApplicationError> { + let mut db = self.db.lock().await; + db.process_queue_with_result(|tx| { + tx.execute( + "UPDATE user_profiles SET is_default = 0 WHERE is_default = 1", + [], + ) + .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; + tx.execute( + "UPDATE user_profiles SET is_default = 1 WHERE id = ? AND \ + name = ?", + params![profile.id, profile.name], + ) + .map_err(|e| ApplicationError::DatabaseError(e.to_string()))?; + Ok(()) + }) + .map_err(ApplicationError::from) + } +} 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 abb8369..acf2c42 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 @@ -261,7 +261,8 @@ pub async fn handle_profile_subcommand( JsonValue::Object(Map::new()) }; - let new_profile = db_handler.create(name, &settings).await?; + let new_profile = + db_handler.create_profile(name, &settings).await?; println!( "Created new profile - ID: {}, Name: {}", new_profile.id, new_profile.name @@ -294,7 +295,7 @@ pub async fn handle_profile_subcommand( settings[key.to_string()] = typed_value; } - db_handler.update(&profile, &settings).await?; + db_handler.update_profile(&profile, &settings).await?; println!( "Profile ID: {} - {} updated. Key '{}' set.", profile.id, profile.name, key @@ -340,7 +341,7 @@ pub async fn handle_profile_subcommand( let mut settings = JsonValue::Object(Map::new()); settings[key.to_string()] = JsonValue::Null; // Null indicates deletion - db_handler.update(&profile, &settings).await?; + db_handler.update_profile(&profile, &settings).await?; println!( "Key '{}' deleted from profile ID: {} - {}.", key, profile.id, profile.name diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/manager.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/manager.rs index 10cae0f..c5ad938 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/manager.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/manager.rs @@ -12,10 +12,6 @@ use super::*; #[async_trait] pub trait ManagedItem: Clone + Send + Sync + ListItemTrait { - async fn save( - &self, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError>; async fn delete( &self, db_handler: &mut UserProfileDbHandler, @@ -34,12 +30,6 @@ pub trait ManagedItem: Clone + Send + Sync + ListItemTrait { #[async_trait] impl ManagedItem for UserProfile { - async fn save( - &self, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError> { - db_handler.update(self, &JsonValue::Null).await - } async fn delete( &self, db_handler: &mut UserProfileDbHandler, @@ -52,7 +42,9 @@ impl ManagedItem for UserProfile { db_handler: &mut UserProfileDbHandler, mask_mode: MaskMode, ) -> Result { - db_handler.get_profile_settings(self, mask_mode).await + db_handler.unlock_profile_settings(&self).await?; + let settings = db_handler.get_profile_settings(self, mask_mode).await?; + Ok(settings) } async fn update_settings( @@ -60,19 +52,12 @@ impl ManagedItem for UserProfile { db_handler: &mut UserProfileDbHandler, settings: &JsonValue, ) -> Result<(), ApplicationError> { - db_handler.update(self, settings).await + db_handler.update_profile(self, settings).await } } #[async_trait] impl ManagedItem for ProviderConfig { - async fn save( - &self, - db_handler: &mut UserProfileDbHandler, - ) -> Result<(), ApplicationError> { - db_handler.save_provider_config(self).await.map(|_| ()) - } - async fn delete( &self, db_handler: &mut UserProfileDbHandler, @@ -89,11 +74,29 @@ impl ManagedItem for ProviderConfig { _db_handler: &mut UserProfileDbHandler, _mask_mode: MaskMode, ) -> Result { - Ok(JsonValue::Object(serde_json::Map::from_iter( - self.additional_settings - .iter() - .map(|(k, v)| (k.clone(), JsonValue::String(v.value.clone()))), - ))) + let mut settings = serde_json::Map::new(); + + // Add provider_type (server) + settings.insert( + "provider_type".to_string(), + JsonValue::String(self.provider_type.clone()), + ); + + // Add model_identifier if present + if let Some(model) = &self.model_identifier { + settings.insert( + "model_identifier".to_string(), + JsonValue::String(model.clone()), + ); + } + + // Add other additional settings + for (key, value) in &self.additional_settings { + settings + .insert(key.clone(), JsonValue::String(value.value.clone())); + } + + Ok(JsonValue::Object(settings)) } async fn update_settings( 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 index 75f9eb4..b908046 100644 --- 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 @@ -23,8 +23,7 @@ use super::{ ConversationEvent, KeyTrack, MaskMode, ModalEvent, ModalWindowTrait, ModalWindowType, ModelServer, ModelSpec, ProviderConfig, ProviderConfigOptions, ReadDocument, ServerTrait, SimpleString, TextLine, - ThreadedChatSession, UserProfile, UserProfileDbHandler, WindowMode, - SUPPORTED_MODEL_ENDPOINTS, + UserProfile, UserProfileDbHandler, WindowMode, SUPPORTED_MODEL_ENDPOINTS, }; #[derive(Debug)] 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 index 8d4e222..4863e36 100644 --- 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 @@ -259,8 +259,9 @@ impl ProfileCreator { } } - let result = - db_handler.create(&new_profile_name, &json!(settings)).await; + let result = db_handler + .create_profile(&new_profile_name, &json!(settings)) + .await; let _ = tx.send(BackgroundTaskResult::ProfileCreated(result)).await; }); diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/textarea.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/textarea.rs index 9f5c66f..341b540 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/textarea.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/textarea.rs @@ -156,11 +156,6 @@ impl<'a, T: TextDocumentTrait> TextAreaState<'a, T> { self.scroll_offset = (self.scroll_offset + amount).min(max_scroll); } - fn max_scroll(&self) -> usize { - let total_lines = self.text_buffer.display_lines_len(); - total_lines.saturating_sub(self.viewport_height) - } - pub fn set_viewport_height(&mut self, height: usize) { self.viewport_height = height; }