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

View File

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

View File

@@ -1,46 +1,36 @@
use axum::{
extract::{Path, State},
response::{IntoResponse, Redirect},
response::IntoResponse,
};
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use serde::Serialize;
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;
page_data!(ContentPageData, "content/base.html", {
#[derive(Serialize)]
pub struct ContentPageData {
user: User,
text_contents: Vec<TextContent>
});
text_contents: Vec<TextContent>,
}
pub async fn show_content_page(
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
RequireUser(user): RequireUser,
) -> 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()),
};
let text_contents = User::get_text_contents(&user.id, &state.db).await?;
let text_contents = User::get_text_contents(&user.id, &state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template(
ContentPageData::template_name(),
Ok(TemplateResponse::new_template(
"content/base.html",
ContentPageData {
user,
text_contents,
},
state.templates,
)?;
Ok(output.into_response())
))
}
#[derive(Serialize)]
@@ -51,57 +41,33 @@ pub struct TextContentEditModal {
pub async fn show_text_content_edit_form(
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
RequireUser(user): RequireUser,
Path(id): Path<String>,
) -> 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()),
};
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template(
Ok(TemplateResponse::new_template(
"content/edit_text_content_modal.html",
TextContentEditModal { user, text_content },
state.templates,
)?;
Ok(output.into_response())
))
}
pub async fn patch_text_content(
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
RequireUser(user): RequireUser,
Path(id): Path<String>,
) -> 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()),
};
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
// ADD FUNCTION TO PATCH CONTENT
let text_contents = User::get_text_contents(&user.id, &state.db)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let text_contents = User::get_text_contents(&user.id, &state.db).await?;
let output = render_template(
Ok(TemplateResponse::new_template(
"content/content_list.html",
ContentPageData {
user,
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?;
// Perform concurrent deletions
join!(
let (_res1, _res2, _res3, _res4, _res5) = join!(
async {
if let Some(file_info) = text_content.file_info {
FileInfo::delete_by_id(&file_info.id, &state.db).await

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use axum::response::Html;
use minijinja_autoreload::AutoReloader;
use crate::error::{HtmlError, IntoHtmlError};
use crate::template_response::HtmlError;
pub mod account;
pub mod admin_panel;
@@ -19,73 +19,19 @@ pub mod signin;
pub mod signout;
pub mod signup;
// pub trait PageData {
// fn template_name() -> &'static str;
// }
// Helper function for render_template
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
// 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
// }
// }
// };
// }
Ok(Html(output))
}

View File

@@ -1,69 +1,42 @@
use axum::{
extract::{Query, State},
response::{IntoResponse, Redirect},
response::IntoResponse,
};
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use composite_retrieval::answer_retrieval::get_answer_with_references;
use serde::{Deserialize, Serialize};
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::routes::HtmlError;
use common::storage::types::user::User;
use crate::{
html_state::HtmlState,
middleware_auth::RequireUser,
template_response::{HtmlError, TemplateResponse},
};
use crate::{html_state::HtmlState, routes::render_template};
#[derive(Deserialize)]
pub struct SearchParams {
query: String,
}
#[derive(Serialize)]
pub struct AnswerData {
user_query: String,
answer_content: String,
answer_references: Vec<String>,
}
pub async fn search_result_handler(
State(state): State<HtmlState>,
Query(query): Query<SearchParams>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
RequireUser(user): RequireUser,
) -> 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 {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
let answer =
get_answer_with_references(&state.db, &state.openai_client, &query.query, &user.id).await?;
// let answer = get_answer_with_references(
// &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(
Ok(TemplateResponse::new_template(
"index/signed_in/search_response.html",
AnswerData {
user_query: query.query,
answer_content: answer,
answer_references: references,
answer_content: answer.content,
answer_references: answer.references,
},
state.templates,
)?;
Ok(output.into_response())
))
}

View File

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