diff --git a/assets/fonts/Satoshi-Regular.ttf b/assets/fonts/Satoshi-Regular.ttf new file mode 100644 index 0000000..fe85cd6 Binary files /dev/null and b/assets/fonts/Satoshi-Regular.ttf differ diff --git a/assets/fonts/Satoshi-Regular.woff b/assets/fonts/Satoshi-Regular.woff new file mode 100644 index 0000000..03ac195 Binary files /dev/null and b/assets/fonts/Satoshi-Regular.woff differ diff --git a/assets/fonts/Satoshi-Regular.woff2 b/assets/fonts/Satoshi-Regular.woff2 new file mode 100644 index 0000000..81c40ab Binary files /dev/null and b/assets/fonts/Satoshi-Regular.woff2 differ diff --git a/crates/common/src/storage/types/message.rs b/crates/common/src/storage/types/message.rs index ca8afbf..f3a2160 100644 --- a/crates/common/src/storage/types/message.rs +++ b/crates/common/src/storage/types/message.rs @@ -35,3 +35,28 @@ impl Message { } } } + +impl fmt::Display for MessageRole { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MessageRole::User => write!(f, "User"), + MessageRole::AI => write!(f, "AI"), + MessageRole::System => write!(f, "System"), + } + } +} + +impl fmt::Display for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.role, self.content) + } +} + +// helper function to format a vector of messages +pub fn format_history(history: &[Message]) -> String { + history + .iter() + .map(|msg| format!("{}", msg)) + .collect::>() + .join("\n") +} diff --git a/crates/common/src/storage/types/text_content.rs b/crates/common/src/storage/types/text_content.rs index e640255..cd0eaae 100644 --- a/crates/common/src/storage/types/text_content.rs +++ b/crates/common/src/storage/types/text_content.rs @@ -1,6 +1,7 @@ +use surrealdb::opt::PatchOp; use uuid::Uuid; -use crate::stored_object; +use crate::{error::AppError, storage::db::SurrealDbClient, stored_object}; use super::file_info::FileInfo; @@ -35,4 +36,24 @@ impl TextContent { user_id, } } + + pub async fn patch( + id: &str, + instructions: &str, + category: &str, + text: &str, + db: &SurrealDbClient, + ) -> Result<(), AppError> { + let now = Utc::now(); + + let _res: Option = db + .update((Self::table_name(), id)) + .patch(PatchOp::replace("/instructions", instructions)) + .patch(PatchOp::replace("/category", category)) + .patch(PatchOp::replace("/text", text)) + .patch(PatchOp::replace("/updated_at", now)) + .await?; + + Ok(()) + } } diff --git a/crates/composite-retrieval/src/answer_retrieval.rs b/crates/composite-retrieval/src/answer_retrieval.rs index 671defa..b5aca58 100644 --- a/crates/composite-retrieval/src/answer_retrieval.rs +++ b/crates/composite-retrieval/src/answer_retrieval.rs @@ -5,6 +5,7 @@ use async_openai::{ CreateChatCompletionRequest, CreateChatCompletionRequestArgs, CreateChatCompletionResponse, ResponseFormat, ResponseFormatJsonSchema, }, + MessageFiles, }; use serde::Deserialize; use serde_json::{json, Value}; @@ -12,7 +13,13 @@ use tracing::debug; use common::{ error::AppError, - storage::{db::SurrealDbClient, types::knowledge_entity::KnowledgeEntity}, + storage::{ + db::SurrealDbClient, + types::{ + knowledge_entity::KnowledgeEntity, + message::{format_history, Message}, + }, + }, }; use crate::retrieve_entities; @@ -109,6 +116,31 @@ pub fn create_user_message(entities_json: &Value, query: &str) -> String { ) } +pub fn create_user_message_with_history( + entities_json: &Value, + history: &[Message], + query: &str, +) -> String { + format!( + r#" + Chat history: + ================== + {} + + Context Information: + ================== + {} + + User Question: + ================== + {} + "#, + format_history(history), + entities_json, + query + ) +} + pub fn create_chat_request( user_message: String, ) -> Result { diff --git a/crates/html-router/src/error.rs b/crates/html-router/src/error.rs deleted file mode 100644 index f0d08f3..0000000 --- a/crates/html-router/src/error.rs +++ /dev/null @@ -1,139 +0,0 @@ -// use axum::{ -// http::StatusCode, -// response::{Html, IntoResponse, Response}, -// }; -// use common::error::AppError; -// use minijinja::context; -// use minijinja_autoreload::AutoReloader; -// use std::sync::Arc; - -// pub type TemplateResult = Result; - -// // Helper trait for converting to HtmlError with templates -// pub trait IntoHtmlError { -// fn with_template(self, templates: Arc) -> HtmlError; -// } -// // // Implement for AppError -// impl IntoHtmlError for AppError { -// fn with_template(self, templates: Arc) -> HtmlError { -// HtmlError::new(self, templates) -// } -// } -// // // Implement for minijinja::Error directly -// impl IntoHtmlError for minijinja::Error { -// fn with_template(self, templates: Arc) -> HtmlError { -// HtmlError::from_template_error(self, templates) -// } -// } - -// pub enum HtmlError { -// ServerError(Arc), -// NotFound(Arc), -// Unauthorized(Arc), -// BadRequest(String, Arc), -// Template(String, Arc), -// } - -// impl HtmlError { -// pub fn new(error: AppError, templates: Arc) -> Self { -// match error { -// AppError::NotFound(_msg) => HtmlError::NotFound(templates), -// AppError::Auth(_msg) => HtmlError::Unauthorized(templates), -// AppError::Validation(msg) => HtmlError::BadRequest(msg, templates), -// _ => { -// tracing::error!("Internal error: {:?}", error); -// HtmlError::ServerError(templates) -// } -// } -// } - -// pub fn from_template_error(error: minijinja::Error, templates: Arc) -> Self { -// tracing::error!("Template error: {:?}", error); -// HtmlError::Template(error.to_string(), templates) -// } -// } - -// impl IntoResponse for HtmlError { -// fn into_response(self) -> Response { -// let (status, context, templates) = match self { -// HtmlError::ServerError(templates) | HtmlError::Template(_, templates) => ( -// StatusCode::INTERNAL_SERVER_ERROR, -// context! { -// status_code => 500, -// title => "Internal Server Error", -// error => "Internal Server Error", -// description => "Something went wrong on our end." -// }, -// templates, -// ), -// HtmlError::NotFound(templates) => ( -// StatusCode::NOT_FOUND, -// context! { -// status_code => 404, -// title => "Page Not Found", -// error => "Not Found", -// description => "The page you're looking for doesn't exist or was removed." -// }, -// templates, -// ), -// HtmlError::Unauthorized(templates) => ( -// StatusCode::UNAUTHORIZED, -// context! { -// status_code => 401, -// title => "Unauthorized", -// error => "Access Denied", -// description => "You need to be logged in to access this page." -// }, -// templates, -// ), -// HtmlError::BadRequest(msg, templates) => ( -// StatusCode::BAD_REQUEST, -// context! { -// status_code => 400, -// title => "Bad Request", -// error => "Bad Request", -// description => msg -// }, -// templates, -// ), -// }; - -// let html = match templates.acquire_env() { -// Ok(env) => match env.get_template("errors/error.html") { -// Ok(tmpl) => match tmpl.render(context) { -// Ok(output) => output, -// Err(e) => { -// tracing::error!("Template render error: {:?}", e); -// Self::fallback_html() -// } -// }, -// Err(e) => { -// tracing::error!("Template get error: {:?}", e); -// Self::fallback_html() -// } -// }, -// Err(e) => { -// tracing::error!("Environment acquire error: {:?}", e); -// Self::fallback_html() -// } -// }; - -// (status, Html(html)).into_response() -// } -// } - -// impl HtmlError { -// fn fallback_html() -> String { -// r#" -// -// -//
-//

Error

-//

Sorry, something went wrong displaying this page.

-//
-// -// -// "# -// .to_string() -// } -// } diff --git a/crates/html-router/src/lib.rs b/crates/html-router/src/lib.rs index c3ccc4c..4a09e7a 100644 --- a/crates/html-router/src/lib.rs +++ b/crates/html-router/src/lib.rs @@ -1,4 +1,3 @@ -pub mod error; pub mod html_state; mod middleware_analytics; mod middleware_auth; diff --git a/crates/html-router/src/routes/chat/message_response_stream.rs b/crates/html-router/src/routes/chat/message_response_stream.rs index 1551e03..f948e5f 100644 --- a/crates/html-router/src/routes/chat/message_response_stream.rs +++ b/crates/html-router/src/routes/chat/message_response_stream.rs @@ -12,7 +12,8 @@ use axum_session_auth::AuthSession; use axum_session_surreal::SessionSurrealPool; use composite_retrieval::{ answer_retrieval::{ - create_chat_request, create_user_message, format_entities_json, LLMResponseFormat, + create_chat_request, create_user_message_with_history, format_entities_json, + LLMResponseFormat, }, retrieve_entities, }; @@ -30,6 +31,7 @@ use tracing::{error, info}; use common::storage::{ db::SurrealDbClient, types::{ + conversation::Conversation, message::{Message, MessageRole}, user::User, }, @@ -50,7 +52,10 @@ async fn get_message_and_user( db: &SurrealDbClient, current_user: Option, message_id: &str, -) -> Result<(Message, User), Sse> + Send>>>> { +) -> Result< + (Message, User, Conversation, Vec), + Sse> + Send>>>, +> { // Check authentication let user = match current_user { Some(user) => user, @@ -77,7 +82,23 @@ async fn get_message_and_user( } }; - Ok((message, user)) + // Get conversation history + let (conversation, mut history) = + match Conversation::get_complete_conversation(&message.conversation_id, &user.id, db).await + { + Err(e) => { + error!("Database error retrieving message {}: {:?}", message_id, e); + return Err(Sse::new(create_error_stream( + "Failed to retrieve message: database error", + ))); + } + Ok((conversation, history)) => (conversation, history), + }; + + // Remove the last message, its the same as the message + history.pop(); + + Ok((message, user, conversation, history)) } #[derive(Deserialize)] @@ -91,9 +112,11 @@ pub async fn get_response_stream( Query(params): Query, ) -> Sse> + Send>>> { // 1. Authentication and initial data validation - let (user_message, user) = + let (user_message, user, _conversation, history) = match get_message_and_user(&state.db, auth.current_user, ¶ms.message_id).await { - Ok((user_message, user)) => (user_message, user), + Ok((user_message, user, conversation, history)) => { + (user_message, user, conversation, history) + } Err(error_stream) => return error_stream, }; @@ -114,7 +137,8 @@ pub async fn get_response_stream( // 3. Create the OpenAI request let entities_json = format_entities_json(&entities); - let formatted_user_message = create_user_message(&entities_json, &user_message.content); + let formatted_user_message = + create_user_message_with_history(&entities_json, &history, &user_message.content); let request = match create_chat_request(formatted_user_message) { Ok(req) => req, Err(..) => { diff --git a/crates/html-router/src/routes/content/mod.rs b/crates/html-router/src/routes/content/mod.rs index cced34e..1bd6082 100644 --- a/crates/html-router/src/routes/content/mod.rs +++ b/crates/html-router/src/routes/content/mod.rs @@ -1,8 +1,9 @@ use axum::{ extract::{Path, State}, response::IntoResponse, + Form, }; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use common::storage::types::{text_content::TextContent, user::User}; @@ -33,12 +34,6 @@ pub async fn show_content_page( )) } -#[derive(Serialize)] -pub struct TextContentEditModal { - pub user: User, - pub text_content: TextContent, -} - pub async fn show_text_content_edit_form( State(state): State, RequireUser(user): RequireUser, @@ -46,20 +41,40 @@ pub async fn show_text_content_edit_form( ) -> Result { let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?; + #[derive(Serialize)] + pub struct TextContentEditModal { + pub user: User, + pub text_content: TextContent, + } + Ok(TemplateResponse::new_template( "content/edit_text_content_modal.html", TextContentEditModal { user, text_content }, )) } +#[derive(Deserialize)] +pub struct PatchTextContentParams { + instructions: String, + category: String, + text: String, +} pub async fn patch_text_content( State(state): State, RequireUser(user): RequireUser, Path(id): Path, + Form(form): Form, ) -> Result { - let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?; + User::get_and_validate_text_content(&id, &user.id, &state.db).await?; - // ADD FUNCTION TO PATCH CONTENT + TextContent::patch( + &id, + &form.instructions, + &form.category, + &form.text, + &state.db, + ) + .await?; let text_contents = User::get_text_contents(&user.id, &state.db).await?; diff --git a/crates/html-router/src/template_response.rs b/crates/html-router/src/template_response.rs index e226552..b26f03a 100644 --- a/crates/html-router/src/template_response.rs +++ b/crates/html-router/src/template_response.rs @@ -261,7 +261,6 @@ impl From for HtmlError { } } -// Now implement IntoResponse for HtmlError impl IntoResponse for HtmlError { fn into_response(self) -> Response { match self { diff --git a/templates/content/edit_text_content_modal.html b/templates/content/edit_text_content_modal.html index e6df340..9b3a387 100644 --- a/templates/content/edit_text_content_modal.html +++ b/templates/content/edit_text_content_modal.html @@ -11,23 +11,21 @@ hx-swap="outerHTML"
- -
{% endblock %} diff --git a/todo.md b/todo.md index 071dde9..f576345 100644 --- a/todo.md +++ b/todo.md @@ -1,19 +1,20 @@ -\[\] fix patch_text_conent \[\] archive ingressed webpage -\[x\] chat styling overhaul \[\] configs primarily get envs \[\] filtering on categories -\[x\] link to ingressed urls or archives \[\] three js graph explorer \[\] three js vector explorer \[x\] add user_id to ingress objects \[x\] admin controls re registration \[x\] chat functionality +\[x\] chat history +\[x\] chat styling overhaul +\[x\] fix patch_text_content \[x\] gdpr \[x\] html ingression \[x\] hx-redirect \[x\] ios shortcut generation \[x\] job queue +\[x\] link to ingressed urls or archives \[x\] macro for pagedata? \[x\] on updates of knowledgeentity create new embeddings \[x\] redirects