refactor: html routes finished

This commit is contained in:
Per Stark
2025-03-15 22:14:17 +01:00
parent 60a0d621e1
commit bbc1cbc302
9 changed files with 227 additions and 551 deletions

View File

@@ -9,10 +9,9 @@ use axum::{
}; };
use axum_session_auth::AuthSession; use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool; use axum_session_surreal::SessionSurrealPool;
use serde::{Deserialize, Serialize};
use surrealdb::{engine::any::Any, Surreal}; use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::routes::HtmlError;
use common::{ use common::{
error::AppError, error::AppError,
storage::types::{ storage::types::{
@@ -22,7 +21,11 @@ use common::{
}, },
}; };
use crate::{html_state::HtmlState, page_data, routes::render_template}; use crate::{
html_state::HtmlState,
middleware_auth::RequireUser,
template_response::{HtmlError, TemplateResponse},
};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ChatStartParams { pub struct ChatStartParams {
@@ -41,25 +44,19 @@ where
serde_json::from_str(&s).map_err(serde::de::Error::custom) serde_json::from_str(&s).map_err(serde::de::Error::custom)
} }
page_data!(ChatData, "chat/base.html", { #[derive(Serialize)]
pub struct ChatPageData {
user: User, user: User,
history: Vec<Message>, history: Vec<Message>,
conversation: Option<Conversation>, conversation: Option<Conversation>,
conversation_archive: Vec<Conversation> conversation_archive: Vec<Conversation>,
}); }
pub async fn show_initialized_chat( pub async fn show_initialized_chat(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Form(form): Form<ChatStartParams>, Form(form): Form<ChatStartParams>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
info!("Displaying chat start");
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/").into_response()),
};
let conversation = Conversation::new(user.id.clone(), "Test".to_owned()); let conversation = Conversation::new(user.id.clone(), "Test".to_owned());
let user_message = Message::new( let user_message = Message::new(
@@ -76,69 +73,48 @@ pub async fn show_initialized_chat(
Some(form.references), Some(form.references),
); );
let (conversation_result, ai_message_result, user_message_result) = futures::join!( state.db.store_item(conversation.clone()).await?;
state.db.store_item(conversation.clone()), state.db.store_item(ai_message.clone()).await?;
state.db.store_item(ai_message.clone()), state.db.store_item(user_message.clone()).await?;
state.db.store_item(user_message.clone())
);
// Check each result individually let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
conversation_result.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
user_message_result.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
ai_message_result.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let messages = vec![user_message, ai_message]; let messages = vec![user_message, ai_message];
let output = render_template( let mut response = TemplateResponse::new_template(
ChatData::template_name(), "chat/base.html",
ChatData { ChatPageData {
history: messages, history: messages,
user, user,
conversation_archive, conversation_archive,
conversation: Some(conversation.clone()), conversation: Some(conversation.clone()),
}, },
state.templates.clone(), )
)?; .into_response();
let mut response = output.into_response();
response.headers_mut().insert( response.headers_mut().insert(
"HX-Push", "HX-Push",
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(), HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
); );
Ok(response) Ok(response)
} }
pub async fn show_chat_base( pub async fn show_chat_base(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
info!("Displaying empty chat start"); let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let user = match auth.current_user { Ok(TemplateResponse::new_template(
Some(user) => user, "chat/base.html",
None => return Ok(Redirect::to("/").into_response()), ChatPageData {
};
let conversation_archive = User::get_user_conversations(&user.id, &state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template(
ChatData::template_name(),
ChatData {
history: vec![], history: vec![],
user, user,
conversation_archive, conversation_archive,
conversation: None, conversation: None,
}, },
state.templates.clone(), ))
)?;
Ok(output.into_response())
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -149,92 +125,61 @@ pub struct NewMessageForm {
pub async fn show_existing_chat( pub async fn show_existing_chat(
Path(conversation_id): Path<String>, Path(conversation_id): Path<String>,
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
info!("Displaying initialized chat with id: {}", conversation_id); let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/").into_response()),
};
let conversation_archive = User::get_user_conversations(&user.id, &state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let (conversation, messages) = let (conversation, messages) =
Conversation::get_complete_conversation(conversation_id.as_str(), &user.id, &state.db) Conversation::get_complete_conversation(conversation_id.as_str(), &user.id, &state.db)
.await .await?;
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template( Ok(TemplateResponse::new_template(
ChatData::template_name(), "chat/base.html",
ChatData { ChatPageData {
history: messages, history: messages,
user, user,
conversation: Some(conversation.clone()), conversation: Some(conversation.clone()),
conversation_archive, conversation_archive,
}, },
state.templates.clone(), ))
)?;
Ok(output.into_response())
} }
pub async fn new_user_message( pub async fn new_user_message(
Path(conversation_id): Path<String>, Path(conversation_id): Path<String>,
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Form(form): Form<NewMessageForm>, Form(form): Form<NewMessageForm>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/").into_response()),
};
let conversation: Conversation = state let conversation: Conversation = state
.db .db
.get_item(&conversation_id) .get_item(&conversation_id)
.await .await?
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))? .ok_or_else(|| AppError::NotFound("Conversation was not found".into()))?;
.ok_or_else(|| {
HtmlError::new(
AppError::NotFound("Conversation was not found".to_string()),
state.templates.clone(),
)
})?;
if conversation.user_id != user.id { if conversation.user_id != user.id {
return Err(HtmlError::new( return Ok(TemplateResponse::unauthorized().into_response());
AppError::Auth("The user does not have permission for this conversation".to_string()),
state.templates.clone(),
));
}; };
let user_message = Message::new(conversation_id, MessageRole::User, form.content, None); let user_message = Message::new(conversation_id, MessageRole::User, form.content, None);
state state.db.store_item(user_message.clone()).await?;
.db
.store_item(user_message.clone())
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
#[derive(Serialize)] #[derive(Serialize)]
struct SSEResponseInitData { struct SSEResponseInitData {
user_message: Message, user_message: Message,
} }
let output = render_template( let mut response = TemplateResponse::new_template(
"chat/streaming_response.html", "chat/streaming_response.html",
SSEResponseInitData { user_message }, SSEResponseInitData { user_message },
state.templates.clone(), )
)?; .into_response();
let mut response = output.into_response();
response.headers_mut().insert( response.headers_mut().insert(
"HX-Push", "HX-Push",
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(), HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
); );
Ok(response) Ok(response)
} }
@@ -256,36 +201,27 @@ pub async fn new_chat_user_message(
None, None,
); );
state state.db.store_item(conversation.clone()).await?;
.db state.db.store_item(user_message.clone()).await?;
.store_item(conversation.clone())
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
state
.db
.store_item(user_message.clone())
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
#[derive(Serialize)] #[derive(Serialize)]
struct SSEResponseInitData { struct SSEResponseInitData {
user_message: Message, user_message: Message,
conversation: Conversation, conversation: Conversation,
} }
let mut response = TemplateResponse::new_template(
let output = render_template(
"chat/new_chat_first_response.html", "chat/new_chat_first_response.html",
SSEResponseInitData { SSEResponseInitData {
user_message, user_message,
conversation: conversation.clone(), conversation: conversation.clone(),
}, },
state.templates.clone(), )
)?; .into_response();
let mut response = output.into_response();
response.headers_mut().insert( response.headers_mut().insert(
"HX-Push", "HX-Push",
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(), HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
); );
Ok(response) Ok(response)
} }

View File

@@ -1,50 +1,33 @@
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{IntoResponse, Redirect}, response::IntoResponse,
}; };
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use serde::Serialize; use serde::Serialize;
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::routes::HtmlError;
use common::{ use common::{
error::AppError, error::AppError,
storage::types::{knowledge_entity::KnowledgeEntity, user::User}, storage::types::{knowledge_entity::KnowledgeEntity, user::User},
}; };
use crate::{html_state::HtmlState, routes::render_template}; use crate::{
html_state::HtmlState,
middleware_auth::RequireUser,
template_response::{HtmlError, TemplateResponse},
};
pub async fn show_reference_tooltip( pub async fn show_reference_tooltip(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Path(reference_id): Path<String>, Path(reference_id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
info!("Showing reference");
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/").into_response()),
};
let entity: KnowledgeEntity = state let entity: KnowledgeEntity = state
.db .db
.get_item(&reference_id) .get_item(&reference_id)
.await .await?
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))? .ok_or_else(|| AppError::NotFound("Item was not found".to_string()))?;
.ok_or_else(|| {
HtmlError::new(
AppError::NotFound("Item was not found".to_string()),
state.templates.clone(),
)
})?;
if entity.user_id != user.id { if entity.user_id != user.id {
return Err(HtmlError::new( return Ok(TemplateResponse::unauthorized());
AppError::Auth("You dont have access to this entity".to_string()),
state.templates.clone(),
));
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -53,11 +36,8 @@ pub async fn show_reference_tooltip(
user: User, user: User,
} }
let output = render_template( Ok(TemplateResponse::new_template(
"chat/reference_tooltip.html", "chat/reference_tooltip.html",
ReferenceTooltipData { entity, user }, ReferenceTooltipData { entity, user },
state.templates.clone(), ))
)?;
Ok(output.into_response())
} }

View File

@@ -1,46 +1,36 @@
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{IntoResponse, Redirect}, response::IntoResponse,
}; };
use axum_session_auth::AuthSession; use serde::Serialize;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use common::storage::types::{text_content::TextContent, user::User}; use common::storage::types::{text_content::TextContent, user::User};
use crate::{error::HtmlError, html_state::HtmlState, page_data}; use crate::{
html_state::HtmlState,
middleware_auth::RequireUser,
template_response::{HtmlError, TemplateResponse},
};
use super::render_template; #[derive(Serialize)]
pub struct ContentPageData {
page_data!(ContentPageData, "content/base.html", {
user: User, user: User,
text_contents: Vec<TextContent> text_contents: Vec<TextContent>,
}); }
pub async fn show_content_page( pub async fn show_content_page(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated let text_contents = User::get_text_contents(&user.id, &state.db).await?;
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
let text_contents = User::get_text_contents(&user.id, &state.db) Ok(TemplateResponse::new_template(
.await "content/base.html",
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template(
ContentPageData::template_name(),
ContentPageData { ContentPageData {
user, user,
text_contents, text_contents,
}, },
state.templates, ))
)?;
Ok(output.into_response())
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -51,57 +41,33 @@ pub struct TextContentEditModal {
pub async fn show_text_content_edit_form( pub async fn show_text_content_edit_form(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Path(id): Path<String>, Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db) Ok(TemplateResponse::new_template(
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template(
"content/edit_text_content_modal.html", "content/edit_text_content_modal.html",
TextContentEditModal { user, text_content }, TextContentEditModal { user, text_content },
state.templates, ))
)?;
Ok(output.into_response())
} }
pub async fn patch_text_content( pub async fn patch_text_content(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Path(id): Path<String>, Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// ADD FUNCTION TO PATCH CONTENT // ADD FUNCTION TO PATCH CONTENT
let text_contents = User::get_text_contents(&user.id, &state.db) let text_contents = User::get_text_contents(&user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template( Ok(TemplateResponse::new_template(
"content/content_list.html", "content/content_list.html",
ContentPageData { ContentPageData {
user, user,
text_contents, text_contents,
}, },
state.templates, ))
)?;
Ok(output.into_response())
} }

View File

@@ -83,7 +83,7 @@ pub async fn delete_text_content(
let text_content = get_and_validate_text_content(&state, &id, &user).await?; let text_content = get_and_validate_text_content(&state, &id, &user).await?;
// Perform concurrent deletions // Perform concurrent deletions
join!( let (_res1, _res2, _res3, _res4, _res5) = join!(
async { async {
if let Some(file_info) = text_content.file_info { if let Some(file_info) = text_content.file_info {
FileInfo::delete_by_id(&file_info.id, &state.db).await FileInfo::delete_by_id(&file_info.id, &state.db).await

View File

@@ -1,12 +1,10 @@
use axum::{ use axum::{
extract::State, extract::State,
response::{Html, IntoResponse, Redirect}, response::{Html, IntoResponse},
}; };
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart}; use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
use futures::{future::try_join_all, TryFutureExt}; use futures::{future::try_join_all, TryFutureExt};
use surrealdb::{engine::any::Any, Surreal}; use serde::Serialize;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use tracing::info; use tracing::info;
@@ -19,47 +17,32 @@ use common::{
}; };
use crate::{ use crate::{
error::{HtmlError, IntoHtmlError},
html_state::HtmlState, html_state::HtmlState,
page_data, middleware_auth::RequireUser,
routes::{index::ActiveJobsData, render_block}, routes::index::ActiveJobsData,
template_response::{HtmlError, TemplateResponse},
}; };
use super::render_template;
#[derive(Serialize)]
pub struct ShowIngressFormData {
user_categories: Vec<String>,
}
pub async fn show_ingress_form( pub async fn show_ingress_form(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
if !auth.is_authenticated() { let user_categories = User::get_user_categories(&user.id, &state.db).await?;
return Ok(Redirect::to("/").into_response());
#[derive(Serialize)]
pub struct ShowIngressFormData {
user_categories: Vec<String>,
} }
let user_categories = User::get_user_categories(&auth.id, &state.db) Ok(TemplateResponse::new_template(
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template(
"index/signed_in/ingress_modal.html", "index/signed_in/ingress_modal.html",
ShowIngressFormData { user_categories }, ShowIngressFormData { user_categories },
state.templates.clone(), ))
)?;
Ok(output.into_response())
} }
pub async fn hide_ingress_form( pub async fn hide_ingress_form(
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(_user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
if !auth.is_authenticated() {
return Ok(Redirect::to("/").into_response());
}
Ok(Html( Ok(Html(
"<a class='btn btn-primary' hx-get='/ingress-form' hx-swap='outerHTML'>Add Content</a>", "<a class='btn btn-primary' hx-get='/ingress-form' hx-swap='outerHTML'>Add Content</a>",
) )
@@ -76,43 +59,39 @@ pub struct IngressParams {
pub files: Vec<FieldData<NamedTempFile>>, pub files: Vec<FieldData<NamedTempFile>>,
} }
page_data!(IngressFormData, "ingress_form.html", {
instructions: String,
content: String,
category: String,
error: String,
});
pub async fn process_ingress_form( pub async fn process_ingress_form(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
TypedMultipart(input): TypedMultipart<IngressParams>, TypedMultipart(input): TypedMultipart<IngressParams>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
let user = auth.current_user.ok_or_else(|| { #[derive(Serialize)]
AppError::Auth("You must be signed in".to_string()).with_template(state.templates.clone()) pub struct IngressFormData {
})?; instructions: String,
content: String,
category: String,
error: String,
}
if input.content.clone().is_some_and(|c| c.len() < 2) && input.files.is_empty() { if input.content.as_ref().map_or(true, |c| c.len() < 2) && input.files.is_empty() {
let output = render_template( return Ok(TemplateResponse::new_template(
IngressFormData::template_name(), "index/signed_in/ingress_form.html",
IngressFormData { IngressFormData {
instructions: input.instructions.clone(), instructions: input.instructions.clone(),
content: input.content.clone().unwrap(), content: input.content.clone().unwrap_or_default(),
category: input.category.clone(), category: input.category.clone(),
error: "You need to either add files or content".to_string(), error: "You need to either add files or content".to_string(),
}, },
state.templates.clone(), ));
)?;
return Ok(output.into_response());
} }
info!("{:?}", input); info!("{:?}", input);
let file_infos = try_join_all(input.files.into_iter().map(|file| { let file_infos = try_join_all(
FileInfo::new(file, &state.db, &user.id) input
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone())) .files
})) .into_iter()
.map(|file| FileInfo::new(file, &state.db, &user.id).map_err(|e| AppError::from(e))),
)
.await?; .await?;
let payloads = IngestionPayload::create_ingestion_payload( let payloads = IngestionPayload::create_ingestion_payload(
@@ -121,8 +100,7 @@ pub async fn process_ingress_form(
input.category, input.category,
file_infos, file_infos,
user.id.as_str(), user.id.as_str(),
) )?;
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let futures: Vec<_> = payloads let futures: Vec<_> = payloads
.into_iter() .into_iter()
@@ -131,25 +109,17 @@ pub async fn process_ingress_form(
}) })
.collect(); .collect();
try_join_all(futures) try_join_all(futures).await?;
.await
.map_err(AppError::from)
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// Update the active jobs page with the newly created job // Update the active jobs page with the newly created job
let active_jobs = User::get_unfinished_ingestion_tasks(&user.id, &state.db) let active_jobs = User::get_unfinished_ingestion_tasks(&user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_block( Ok(TemplateResponse::new_partial(
"index/signed_in/active_jobs.html", "index/signed_in/active_jobs.html",
"active_jobs_section", "active_jobs_section",
ActiveJobsData { ActiveJobsData {
user: user.clone(), user: user.clone(),
active_jobs, active_jobs,
}, },
state.templates.clone(), ))
)?;
Ok(output.into_response())
} }

View File

@@ -1,55 +1,42 @@
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{IntoResponse, Redirect}, response::IntoResponse,
Form, Form,
}; };
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use plotly::{ use plotly::{
common::{Line, Marker, Mode}, common::{Line, Marker, Mode},
layout::{Axis, Camera, LayoutScene, ProjectionType}, layout::{Axis, Camera, LayoutScene, ProjectionType},
Layout, Plot, Scatter3D, Layout, Plot, Scatter3D,
}; };
use surrealdb::{engine::any::Any, Surreal}; use serde::{Deserialize, Serialize};
use tracing::info;
use common::{ use common::storage::types::{
error::AppError, knowledge_entity::{KnowledgeEntity, KnowledgeEntityType},
storage::types::{ knowledge_relationship::KnowledgeRelationship,
knowledge_entity::{KnowledgeEntity, KnowledgeEntityType}, user::User,
knowledge_relationship::KnowledgeRelationship,
user::User,
},
}; };
use crate::{error::HtmlError, html_state::HtmlState, page_data, routes::render_template}; use crate::{
html_state::HtmlState,
page_data!(KnowledgeBaseData, "knowledge/base.html", { middleware_auth::RequireUser,
entities: Vec<KnowledgeEntity>, template_response::{HtmlError, TemplateResponse},
relationships: Vec<KnowledgeRelationship>, };
user: User,
plot_html: String
});
pub async fn show_knowledge_page( pub async fn show_knowledge_page(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated #[derive(Serialize)]
let user = match auth.current_user { pub struct KnowledgeBaseData {
Some(user) => user, entities: Vec<KnowledgeEntity>,
None => return Ok(Redirect::to("/signin").into_response()), relationships: Vec<KnowledgeRelationship>,
}; user: User,
plot_html: String,
}
let entities = User::get_knowledge_entities(&user.id, &state.db) let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
info!("Got entities ok"); let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
let relationships = User::get_knowledge_relationships(&user.id, &state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let mut plot = Plot::new(); let mut plot = Plot::new();
@@ -124,40 +111,32 @@ pub async fn show_knowledge_page(
.plot_background_color("rbga(0,0,0,0)"); .plot_background_color("rbga(0,0,0,0)");
plot.set_layout(layout); plot.set_layout(layout);
// Convert to HTML // Convert to HTML
let html = plot.to_html(); let html = plot.to_html();
let output = render_template( Ok(TemplateResponse::new_template(
KnowledgeBaseData::template_name(), "knowledge/base.html",
KnowledgeBaseData { KnowledgeBaseData {
entities, entities,
relationships, relationships,
user, user,
plot_html: html, plot_html: html,
}, },
state.templates, ))
)?;
Ok(output.into_response())
}
#[derive(Serialize)]
pub struct EntityData {
entity: KnowledgeEntity,
entity_types: Vec<String>,
user: User,
} }
pub async fn show_edit_knowledge_entity_form( pub async fn show_edit_knowledge_entity_form(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Path(id): Path<String>, Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated #[derive(Serialize)]
let user = match auth.current_user { pub struct EntityData {
Some(user) => user, entity: KnowledgeEntity,
None => return Ok(Redirect::to("/signin").into_response()), entity_types: Vec<String>,
}; user: User,
}
// Get entity types // Get entity types
let entity_types: Vec<String> = KnowledgeEntityType::variants() let entity_types: Vec<String> = KnowledgeEntityType::variants()
@@ -166,27 +145,16 @@ pub async fn show_edit_knowledge_entity_form(
.collect(); .collect();
// Get the entity and validate ownership // Get the entity and validate ownership
let entity = User::get_and_validate_knowledge_entity(&id, &user.id, &state.db) let entity = User::get_and_validate_knowledge_entity(&id, &user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template( Ok(TemplateResponse::new_template(
"knowledge/edit_knowledge_entity_modal.html", "knowledge/edit_knowledge_entity_modal.html",
EntityData { EntityData {
entity, entity,
user, user,
entity_types, entity_types,
}, },
state.templates, ))
)?;
Ok(output.into_response())
}
#[derive(Serialize)]
pub struct EntityListData {
entities: Vec<KnowledgeEntity>,
user: User,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -197,21 +165,19 @@ pub struct PatchKnowledgeEntityParams {
pub description: String, pub description: String,
} }
#[derive(Serialize)]
pub struct EntityListData {
entities: Vec<KnowledgeEntity>,
user: User,
}
pub async fn patch_knowledge_entity( pub async fn patch_knowledge_entity(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Form(form): Form<PatchKnowledgeEntityParams>, Form(form): Form<PatchKnowledgeEntityParams>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
// Get the existing entity and validate that the user is allowed // Get the existing entity and validate that the user is allowed
User::get_and_validate_knowledge_entity(&form.id, &user.id, &state.db) User::get_and_validate_knowledge_entity(&form.id, &user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let entity_type: KnowledgeEntityType = KnowledgeEntityType::from(form.entity_type); let entity_type: KnowledgeEntityType = KnowledgeEntityType::from(form.entity_type);
@@ -224,60 +190,36 @@ pub async fn patch_knowledge_entity(
&state.db, &state.db,
&state.openai_client, &state.openai_client,
) )
.await .await?;
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
// Get updated list of entities // Get updated list of entities
let entities = User::get_knowledge_entities(&user.id, &state.db) let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// Render updated list // Render updated list
let output = render_template( Ok(TemplateResponse::new_template(
"knowledge/entity_list.html", "knowledge/entity_list.html",
EntityListData { entities, user }, EntityListData { entities, user },
state.templates, ))
)?;
Ok(output.into_response())
} }
pub async fn delete_knowledge_entity( pub async fn delete_knowledge_entity(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Path(id): Path<String>, Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
// Get the existing entity and validate that the user is allowed // Get the existing entity and validate that the user is allowed
User::get_and_validate_knowledge_entity(&id, &user.id, &state.db) User::get_and_validate_knowledge_entity(&id, &user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// Delete the entity // Delete the entity
state state.db.delete_item::<KnowledgeEntity>(&id).await?;
.db
.delete_item::<KnowledgeEntity>(&id)
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
// Get updated list of entities // Get updated list of entities
let entities = User::get_knowledge_entities(&user.id, &state.db) let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// Render updated list Ok(TemplateResponse::new_template(
let output = render_template(
"knowledge/entity_list.html", "knowledge/entity_list.html",
EntityListData { entities, user }, EntityListData { entities, user },
state.templates, ))
)?;
Ok(output.into_response())
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -288,40 +230,25 @@ pub struct RelationshipTableData {
pub async fn delete_knowledge_relationship( pub async fn delete_knowledge_relationship(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Path(id): Path<String>, Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
// GOTTA ADD AUTH VALIDATION // GOTTA ADD AUTH VALIDATION
KnowledgeRelationship::delete_relationship_by_id(&id, &state.db) KnowledgeRelationship::delete_relationship_by_id(&id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let entities = User::get_knowledge_entities(&user.id, &state.db) let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let relationships = User::get_knowledge_relationships(&user.id, &state.db) let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// Render updated list // Render updated list
let output = render_template( Ok(TemplateResponse::new_template(
"knowledge/relationship_table.html", "knowledge/relationship_table.html",
RelationshipTableData { RelationshipTableData {
entities, entities,
relationships, relationships,
}, },
state.templates, ))
)?;
Ok(output.into_response())
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -333,15 +260,9 @@ pub struct SaveKnowledgeRelationshipInput {
pub async fn save_knowledge_relationship( pub async fn save_knowledge_relationship(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
Form(form): Form<SaveKnowledgeRelationshipInput>, Form(form): Form<SaveKnowledgeRelationshipInput>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
// Construct relationship // Construct relationship
let relationship = KnowledgeRelationship::new( let relationship = KnowledgeRelationship::new(
form.in_, form.in_,
@@ -351,28 +272,18 @@ pub async fn save_knowledge_relationship(
form.relationship_type, form.relationship_type,
); );
relationship relationship.store_relationship(&state.db).await?;
.store_relationship(&state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let entities = User::get_knowledge_entities(&user.id, &state.db) let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let relationships = User::get_knowledge_relationships(&user.id, &state.db) let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// Render updated list // Render updated list
let output = render_template( Ok(TemplateResponse::new_template(
"knowledge/relationship_table.html", "knowledge/relationship_table.html",
RelationshipTableData { RelationshipTableData {
entities, entities,
relationships, relationships,
}, },
state.templates, ))
)?;
Ok(output.into_response())
} }

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use axum::response::Html; use axum::response::Html;
use minijinja_autoreload::AutoReloader; use minijinja_autoreload::AutoReloader;
use crate::error::{HtmlError, IntoHtmlError}; use crate::template_response::HtmlError;
pub mod account; pub mod account;
pub mod admin_panel; pub mod admin_panel;
@@ -19,73 +19,19 @@ pub mod signin;
pub mod signout; pub mod signout;
pub mod signup; pub mod signup;
// pub trait PageData { // Helper function for render_template
// fn template_name() -> &'static str; pub fn render_template<T>(
// } template_name: &str,
context: T,
templates: Arc<AutoReloader>,
) -> Result<Html<String>, HtmlError>
where
T: serde::Serialize,
{
let env = templates.acquire_env().unwrap();
let tmpl = env.get_template(template_name).unwrap();
let context = minijinja::Value::from_serialize(&context);
let output = tmpl.render(context).unwrap();
// // Helper function for render_template Ok(Html(output))
// pub fn render_template<T>( }
// template_name: &str,
// context: T,
// templates: Arc<AutoReloader>,
// ) -> Result<Html<String>, HtmlError>
// where
// T: serde::Serialize,
// {
// let env = templates
// .acquire_env()
// .map_err(|e| e.with_template(templates.clone()))?;
// let tmpl = env
// .get_template(template_name)
// .map_err(|e| e.with_template(templates.clone()))?;
// let context = minijinja::Value::from_serialize(&context);
// let output = tmpl
// .render(context)
// .map_err(|e| e.with_template(templates.clone()))?;
// Ok(Html(output))
// }
// pub fn render_block<T>(
// template_name: &str,
// block: &str,
// context: T,
// templates: Arc<AutoReloader>,
// ) -> Result<Html<String>, HtmlError>
// where
// T: serde::Serialize,
// {
// let env = templates
// .acquire_env()
// .map_err(|e| e.with_template(templates.clone()))?;
// let tmpl = env
// .get_template(template_name)
// .map_err(|e| e.with_template(templates.clone()))?;
// let context = minijinja::Value::from_serialize(&context);
// let output = tmpl
// .eval_to_state(context)
// .map_err(|e| e.with_template(templates.clone()))?
// .render_block(block)
// .map_err(|e| e.with_template(templates.clone()))?;
// Ok(output.into())
// }
// #[macro_export]
// macro_rules! page_data {
// ($name:ident, $template_name:expr, {$($(#[$attr:meta])* $field:ident: $ty:ty),*$(,)?}) => {
// use serde::{Serialize, Deserialize};
// use $crate::routes::PageData;
// #[derive(Debug, Deserialize, Serialize)]
// pub struct $name {
// $($(#[$attr])* pub $field: $ty),*
// }
// impl PageData for $name {
// fn template_name() -> &'static str {
// $template_name
// }
// }
// };
// }

View File

@@ -1,69 +1,42 @@
use axum::{ use axum::{
extract::{Query, State}, extract::{Query, State},
response::{IntoResponse, Redirect}, response::IntoResponse,
}; };
use axum_session_auth::AuthSession; use composite_retrieval::answer_retrieval::get_answer_with_references;
use axum_session_surreal::SessionSurrealPool;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::routes::HtmlError; use crate::{
use common::storage::types::user::User; html_state::HtmlState,
middleware_auth::RequireUser,
template_response::{HtmlError, TemplateResponse},
};
use crate::{html_state::HtmlState, routes::render_template};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct SearchParams { pub struct SearchParams {
query: String, query: String,
} }
#[derive(Serialize)]
pub struct AnswerData {
user_query: String,
answer_content: String,
answer_references: Vec<String>,
}
pub async fn search_result_handler( pub async fn search_result_handler(
State(state): State<HtmlState>, State(state): State<HtmlState>,
Query(query): Query<SearchParams>, Query(query): Query<SearchParams>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>, RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
info!("Displaying search results"); #[derive(Serialize)]
pub struct AnswerData {
user_query: String,
answer_content: String,
answer_references: Vec<String>,
}
let user = match auth.current_user { let answer =
Some(user) => user, get_answer_with_references(&state.db, &state.openai_client, &query.query, &user.id).await?;
None => return Ok(Redirect::to("/signin").into_response()),
};
// let answer = get_answer_with_references( Ok(TemplateResponse::new_template(
// &state.surreal_db_client,
// &state.openai_client,
// &query.query,
// &user.id,
// )
// .await
// .map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let answer = "The Minne project is focused on simplifying knowledge management through features such as easy capture, smart analysis, and visualization of connections between ideas. It includes various functionalities like the Smart Analysis Feature, which provides content analysis and organization, and the Easy Capture Feature, which allows users to effortlessly capture and retrieve knowledge in various formats. Additionally, it offers tools like Knowledge Graph Visualization to enhance understanding and organization of knowledge. The project also emphasizes a user-friendly onboarding experience and mobile-friendly options for accessing its services.".to_string();
let references = vec![
"i81cd5be8-557c-4b2b-ba3a-4b8d28e74b9b".to_string(),
"5f72a724-d7a3-467d-8783-7cca6053ddc7".to_string(),
"ad106a1f-ccda-415e-9e87-c3a34e202624".to_string(),
"8797b57d-094d-4ee9-a3a7-c3195b246254".to_string(),
"69763f43-82e6-4cb5-ba3e-f6da13777dab".to_string(),
];
let output = render_template(
"index/signed_in/search_response.html", "index/signed_in/search_response.html",
AnswerData { AnswerData {
user_query: query.query, user_query: query.query,
answer_content: answer, answer_content: answer.content,
answer_references: references, answer_references: answer.references,
}, },
state.templates, ))
)?;
Ok(output.into_response())
} }

View File

@@ -10,7 +10,7 @@ use minijinja_autoreload::AutoReloader;
use serde::Serialize; use serde::Serialize;
use std::sync::Arc; use std::sync::Arc;
use crate::{html_state::HtmlState, AuthSessionType}; use crate::html_state::HtmlState;
// Enum for template types // Enum for template types
#[derive(Clone)] #[derive(Clone)]
@@ -113,10 +113,8 @@ impl IntoResponse for TemplateResponse {
} }
} }
// Wrapper to avoid recursion
struct TemplateStateWrapper { struct TemplateStateWrapper {
state: HtmlState, state: HtmlState,
auth: AuthSessionType,
template_response: TemplateResponse, template_response: TemplateResponse,
} }
@@ -228,7 +226,6 @@ fn fallback_error() -> Html<String> {
pub async fn with_template_response( pub async fn with_template_response(
State(state): State<HtmlState>, State(state): State<HtmlState>,
auth: AuthSessionType,
response: Response, response: Response,
) -> Response { ) -> Response {
// Clone the TemplateResponse from extensions // Clone the TemplateResponse from extensions
@@ -237,7 +234,6 @@ pub async fn with_template_response(
if let Some(template_response) = template_response { if let Some(template_response) = template_response {
TemplateStateWrapper { TemplateStateWrapper {
state, state,
auth,
template_response, template_response,
} }
.into_response() .into_response()
@@ -249,7 +245,6 @@ pub async fn with_template_response(
// Define HtmlError // Define HtmlError
pub enum HtmlError { pub enum HtmlError {
AppError(AppError), AppError(AppError),
TemplateError(String),
} }
// Conversion from AppError to HtmlError // Conversion from AppError to HtmlError
@@ -282,7 +277,6 @@ impl IntoResponse for HtmlError {
}; };
template_response.into_response() template_response.into_response()
} }
HtmlError::TemplateError(_) => TemplateResponse::server_error().into_response(),
} }
} }
} }