diff --git a/assets/style.css b/assets/style.css index 1bb342d..6071af4 100644 --- a/assets/style.css +++ b/assets/style.css @@ -2406,6 +2406,9 @@ } } } + .z-4 { + z-index: 4; + } .tab-content { order { } @@ -3503,6 +3506,9 @@ .h-5 { height: calc(var(--spacing) * 5); } + .h-6 { + height: calc(var(--spacing) * 6); + } .h-32 { height: calc(var(--spacing) * 32); } @@ -3512,6 +3518,9 @@ .min-h-screen { min-height: 100vh; } + .w-2 { + width: calc(var(--spacing) * 2); + } .w-5 { width: calc(var(--spacing) * 5); } diff --git a/src/ingress/analysis/types/llm_analysis_result.rs b/src/ingress/analysis/types/llm_analysis_result.rs index 8e72927..6ec9442 100644 --- a/src/ingress/analysis/types/llm_analysis_result.rs +++ b/src/ingress/analysis/types/llm_analysis_result.rs @@ -66,7 +66,7 @@ impl LLMGraphAnalysisResult { .await?; // Process relationships - let relationships = self.process_relationships(Arc::clone(&mapper))?; + let relationships = self.process_relationships(source_id, Arc::clone(&mapper))?; Ok((entities, relationships)) } @@ -116,6 +116,7 @@ impl LLMGraphAnalysisResult { fn process_relationships( &self, + source_id: &str, mapper: Arc>, ) -> Result, AppError> { let mut mapper_guard = mapper @@ -130,6 +131,7 @@ impl LLMGraphAnalysisResult { Ok(KnowledgeRelationship::new( source_db_id.to_string(), target_db_id.to_string(), + source_id.to_string(), rel.type_.clone(), None, )) diff --git a/src/server/routes/html/index.rs b/src/server/routes/html/index.rs index 0e701b3..a7e6f50 100644 --- a/src/server/routes/html/index.rs +++ b/src/server/routes/html/index.rs @@ -16,8 +16,12 @@ use crate::{ AppState, }, storage::{ - db::delete_item, - types::{job::Job, text_content::TextContent, user::User}, + db::{delete_item, get_all_stored_items, get_item}, + types::{ + file_info::FileInfo, job::Job, knowledge_entity::KnowledgeEntity, + knowledge_relationship::KnowledgeRelationship, text_chunk::TextChunk, + text_content::TextContent, user::User, + }, }, }; @@ -56,19 +60,15 @@ pub async fn index_handler( false => vec![], }; - info!("{:?}", latest_text_contents); - - let latest_knowledge_entities = match auth.current_user.is_some() { - true => User::get_latest_knowledge_entities( - auth.current_user.clone().unwrap().id.as_str(), - &state.surreal_db_client, - ) - .await - .map_err(|e| HtmlError::new(e, state.templates.clone()))?, - false => vec![], - }; - - info!("{:?}", latest_knowledge_entities); + // let latest_knowledge_entities = match auth.current_user.is_some() { + // true => User::get_latest_knowledge_entities( + // auth.current_user.clone().unwrap().id.as_str(), + // &state.surreal_db_client, + // ) + // .await + // .map_err(|e| HtmlError::new(e, state.templates.clone()))?, + // false => vec![], + // }; let output = render_template( IndexData::template_name(), @@ -100,15 +100,65 @@ pub async fn delete_text_content( None => return Ok(Redirect::to("/").into_response()), }; - delete_item::(&state.surreal_db_client, &id) + // Get TextContent from db + let text_content = match get_item::(&state.surreal_db_client, &id) + .await + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))? + { + Some(text_content) => text_content, + None => { + return Err(HtmlError::new( + AppError::NotFound("No item found".to_string()), + state.templates, + )) + } + }; + + // Validate that the user is the owner + if text_content.user_id != user.id { + return Err(HtmlError::new( + AppError::Auth("You are not the owner of that content".to_string()), + state.templates, + )); + } + + // If TextContent has file_info, delete it from db and file from disk. + if text_content.file_info.is_some() { + FileInfo::delete_by_id( + &text_content.file_info.unwrap().id, + &state.surreal_db_client, + ) + .await + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; + } + + // Delete textcontent from db + delete_item::(&state.surreal_db_client, &text_content.id) .await .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; - let latest_text_contents = User::get_latest_text_contents(&user.id, &state.surreal_db_client) + // Delete TextChunks + TextChunk::delete_by_source_id(&text_content.id, &state.surreal_db_client) .await .map_err(|e| HtmlError::new(e, state.templates.clone()))?; - info!("{:?}", latest_text_contents); + // Delete KnowledgeEntities + KnowledgeEntity::delete_by_source_id(&text_content.id, &state.surreal_db_client) + .await + .map_err(|e| HtmlError::new(e, state.templates.clone()))?; + + // Delete KnowledgeRelationships + KnowledgeRelationship::delete_relationships_by_source_id( + &text_content.id, + &state.surreal_db_client, + ) + .await + .map_err(|e| HtmlError::new(e, state.templates.clone()))?; + + // Get latest text contents after updates + let latest_text_contents = User::get_latest_text_contents(&user.id, &state.surreal_db_client) + .await + .map_err(|e| HtmlError::new(e, state.templates.clone()))?; let output = render_block( "index/signed_in/recent_content.html", diff --git a/src/storage/types/file_info.rs b/src/storage/types/file_info.rs index 539ce10..688b0dd 100644 --- a/src/storage/types/file_info.rs +++ b/src/storage/types/file_info.rs @@ -7,11 +7,12 @@ use std::{ }; use tempfile::NamedTempFile; use thiserror::Error; +use tokio::fs::remove_dir_all; use tracing::info; use uuid::Uuid; use crate::{ - storage::db::{store_item, SurrealDbClient}, + storage::db::{delete_item, get_item, store_item, SurrealDbClient}, stored_object, }; @@ -198,4 +199,50 @@ impl FileInfo { .next() .ok_or(FileError::FileNotFound(sha256.to_string())) } + + /// Removes FileInfo from database and file from disk + /// + /// # Arguments + /// * `id` - Id of the FileInfo + /// * `db_client` - Reference to SurrealDbClient + /// + /// # Returns + /// `Result<(), FileError>` + pub async fn delete_by_id(id: &str, db_client: &SurrealDbClient) -> Result<(), FileError> { + // Get the FileInfo from the database + let file_info = match get_item::(db_client, id).await? { + Some(info) => info, + None => { + return Err(FileError::FileNotFound(format!( + "File with id {} was not found", + id + ))) + } + }; + + // Remove the file and its parent directory + let file_path = Path::new(&file_info.path); + if file_path.exists() { + // Get the parent directory of the file + if let Some(parent_dir) = file_path.parent() { + // Remove the entire directory containing the file + remove_dir_all(parent_dir).await?; + info!("Removed directory {:?} and its contents", parent_dir); + } else { + return Err(FileError::FileNotFound( + "File has no parent directory".to_string(), + )); + } + } else { + return Err(FileError::FileNotFound(format!( + "File at path {:?} was not found", + file_path + ))); + } + + // Delete the FileInfo from the database + delete_item::(db_client, id).await?; + + Ok(()) + } } diff --git a/src/storage/types/knowledge_entity.rs b/src/storage/types/knowledge_entity.rs index 93df546..9ab3326 100644 --- a/src/storage/types/knowledge_entity.rs +++ b/src/storage/types/knowledge_entity.rs @@ -1,4 +1,4 @@ -use crate::stored_object; +use crate::{error::AppError, storage::db::SurrealDbClient, stored_object}; use uuid::Uuid; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -58,4 +58,18 @@ impl KnowledgeEntity { user_id, } } + + pub async fn delete_by_source_id( + source_id: &str, + db_client: &SurrealDbClient, + ) -> Result<(), AppError> { + let query = format!( + "DELETE {} WHERE source_id = '{}'", + Self::table_name(), + source_id + ); + db_client.query(query).await?; + + Ok(()) + } } diff --git a/src/storage/types/knowledge_relationship.rs b/src/storage/types/knowledge_relationship.rs index 1786b26..178d1c7 100644 --- a/src/storage/types/knowledge_relationship.rs +++ b/src/storage/types/knowledge_relationship.rs @@ -1,13 +1,14 @@ -use crate::{error::AppError, stored_object}; -use surrealdb::{engine::any::Any, Surreal}; +use crate::{error::AppError, storage::db::SurrealDbClient, stored_object}; +use surrealdb::{engine::any::Any, sql::Subquery, Surreal}; use tracing::debug; use uuid::Uuid; -stored_object!(KnowledgeRelationship, "knowledge_relationship", { +stored_object!(KnowledgeRelationship, "relates_to", { #[serde(rename = "in")] in_: String, out: String, relationship_type: String, + source_id: String, metadata: Option }); @@ -15,6 +16,7 @@ impl KnowledgeRelationship { pub fn new( in_: String, out: String, + source_id: String, relationship_type: String, metadata: Option, ) -> Self { @@ -25,6 +27,7 @@ impl KnowledgeRelationship { updated_at: now, in_, out, + source_id, relationship_type, metadata, } @@ -41,4 +44,18 @@ impl KnowledgeRelationship { Ok(()) } + + pub async fn delete_relationships_by_source_id( + source_id: &str, + db_client: &SurrealDbClient, + ) -> Result<(), AppError> { + let query = format!( + "DELETE knowledge_entity -> relates_to WHERE source_id = '{}'", + source_id + ); + + db_client.query(query).await?; + + Ok(()) + } } diff --git a/src/storage/types/text_chunk.rs b/src/storage/types/text_chunk.rs index d75c4bb..9b56699 100644 --- a/src/storage/types/text_chunk.rs +++ b/src/storage/types/text_chunk.rs @@ -1,4 +1,4 @@ -use crate::stored_object; +use crate::{error::AppError, storage::db::SurrealDbClient, stored_object}; use uuid::Uuid; stored_object!(TextChunk, "text_chunk", { @@ -21,4 +21,18 @@ impl TextChunk { user_id, } } + + pub async fn delete_by_source_id( + source_id: &str, + db_client: &SurrealDbClient, + ) -> Result<(), AppError> { + let query = format!( + "DELETE {} WHERE source_id = '{}'", + Self::table_name(), + source_id + ); + db_client.query(query).await?; + + Ok(()) + } } diff --git a/src/storage/types/text_content.rs b/src/storage/types/text_content.rs index 8f50e70..e640255 100644 --- a/src/storage/types/text_content.rs +++ b/src/storage/types/text_content.rs @@ -7,7 +7,6 @@ use super::file_info::FileInfo; stored_object!(TextContent, "text_content", { text: String, file_info: Option, - url: Option, instructions: String, category: String,