multi chat and history, oob swap

This commit is contained in:
Per Stark
2025-02-28 12:02:16 +01:00
parent 21f0ebef33
commit 65c5900189
12 changed files with 421 additions and 44 deletions

View File

@@ -1,4 +1,5 @@
pub mod message_response_stream;
pub mod references;
use axum::{
extract::{Path, State},
@@ -16,7 +17,7 @@ use crate::{
page_data,
server::{routes::html::render_template, AppState},
storage::{
db::store_item,
db::{get_item, store_item},
types::{
conversation::Conversation,
message::{Message, MessageRole},
@@ -46,7 +47,7 @@ where
page_data!(ChatData, "chat/base.html", {
user: User,
history: Vec<Message>,
conversation: Conversation,
conversation: Option<Conversation>,
conversation_archive: Vec<Conversation>
});
@@ -101,7 +102,7 @@ pub async fn show_initialized_chat(
history: messages,
user,
conversation_archive,
conversation: conversation.clone(),
conversation: Some(conversation.clone()),
},
state.templates.clone(),
)?;
@@ -129,15 +130,13 @@ pub async fn show_chat_base(
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let conversation = Conversation::new(user.id.clone(), "New Chat".to_string());
let output = render_template(
ChatData::template_name(),
ChatData {
history: vec![],
user,
conversation_archive,
conversation,
conversation: None,
},
state.templates.clone(),
)?;
@@ -179,7 +178,7 @@ pub async fn show_existing_chat(
ChatData {
history: messages,
user,
conversation: conversation.clone(),
conversation: Some(conversation.clone()),
conversation_archive,
},
state.templates.clone(),
@@ -194,11 +193,28 @@ pub async fn new_user_message(
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<NewMessageForm>,
) -> Result<impl IntoResponse, HtmlError> {
let _user = match auth.current_user {
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/").into_response()),
};
let conversation: Conversation = get_item(&state.surreal_db_client, &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(),
)
})?;
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(),
));
};
let user_message = Message::new(conversation_id, MessageRole::User, form.content, None);
store_item(&state.surreal_db_client, user_message.clone())
@@ -216,5 +232,58 @@ pub async fn new_user_message(
state.templates.clone(),
)?;
Ok(output.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 new_chat_user_message(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
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::new(user.id, "New chat".to_string());
let user_message = Message::new(
conversation.id.clone(),
MessageRole::User,
form.content,
None,
);
store_item(&state.surreal_db_client, conversation.clone())
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
store_item(&state.surreal_db_client, user_message.clone())
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
#[derive(Serialize)]
struct SSEResponseInitData {
user_message: Message,
conversation: Conversation,
}
let output = render_template(
"chat/new_chat_first_response.html",
SSEResponseInitData {
user_message,
conversation: conversation.clone(),
},
state.templates.clone(),
)?;
let mut response = output.into_response();
response.headers_mut().insert(
"HX-Push",
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
);
Ok(response)
}

View File

@@ -0,0 +1,62 @@
use axum::{
extract::{Path, State},
response::{IntoResponse, Redirect},
};
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use serde::Serialize;
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::{
error::{AppError, HtmlError},
server::{routes::html::render_template, AppState},
storage::{
db::get_item,
types::{knowledge_entity::KnowledgeEntity, user::User},
},
};
pub async fn show_reference_tooltip(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
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 = get_item(&state.surreal_db_client, &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(),
)
})?;
if entity.user_id != user.id {
return Err(HtmlError::new(
AppError::Auth("You dont have access to this entity".to_string()),
state.templates.clone(),
));
}
#[derive(Serialize)]
struct ReferenceTooltipData {
entity: KnowledgeEntity,
user: User,
}
let output = render_template(
"chat/reference_tooltip.html",
ReferenceTooltipData { entity, user },
state.templates.clone(),
)?;
Ok(output.into_response())
}

View File

@@ -17,8 +17,9 @@ use html::{
account::{delete_account, set_api_key, show_account_page, update_timezone},
admin_panel::{show_admin_panel, toggle_registration_status},
chat::{
message_response_stream::get_response_stream, new_user_message, show_chat_base,
show_existing_chat, show_initialized_chat,
message_response_stream::get_response_stream, new_chat_user_message, new_user_message,
references::show_reference_tooltip, show_chat_base, show_existing_chat,
show_initialized_chat,
},
content::{patch_text_content, show_content_page, show_text_content_edit_form},
documentation::{
@@ -67,9 +68,11 @@ pub fn html_routes(app_state: &AppState) -> Router<AppState> {
.route("/gdpr/accept", post(accept_gdpr))
.route("/gdpr/deny", post(deny_gdpr))
.route("/search", get(search_result_handler))
.route("/chat", get(show_chat_base).post(show_initialized_chat))
.route("/chat", get(show_chat_base).post(new_chat_user_message))
.route("/initialized-chat", post(show_initialized_chat))
.route("/chat/:id", get(show_existing_chat).post(new_user_message))
.route("/chat/response-stream", get(get_response_stream))
.route("/knowledge/:id", get(show_reference_tooltip))
.route("/signout", get(sign_out_user))
.route("/signin", get(show_signin_form).post(authenticate_user))
.route(

View File

@@ -34,4 +34,21 @@ impl Message {
references,
}
}
pub fn new_ai_message(
conversation_id: String,
id: String,
content: String,
references: Option<Vec<String>>,
) -> Self {
let now = Utc::now();
Self {
id,
created_at: now,
updated_at: now,
role: MessageRole::AI,
content,
references,
conversation_id,
}
}
}