mirror of
https://github.com/perstarkse/minne.git
synced 2026-03-21 17:09:51 +01:00
wip: chat interface
This commit is contained in:
5797
assets/style.css
5797
assets/style.css
File diff suppressed because one or more lines are too long
124
src/server/routes/html/chat/mod.rs
Normal file
124
src/server/routes/html/chat/mod.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect},
|
||||
Form,
|
||||
};
|
||||
use axum_session_auth::AuthSession;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use surrealdb::{engine::any::Any, Surreal};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
error::HtmlError,
|
||||
page_data,
|
||||
server::{routes::html::render_template, AppState},
|
||||
storage::types::user::User,
|
||||
};
|
||||
|
||||
// Update your ChatStartParams struct to properly deserialize the references
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ChatStartParams {
|
||||
user_query: String,
|
||||
llm_response: String,
|
||||
#[serde(deserialize_with = "deserialize_references")]
|
||||
references: Vec<String>,
|
||||
}
|
||||
|
||||
// Custom deserializer function
|
||||
fn deserialize_references<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
serde_json::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
page_data!(ChatData, "chat/base.html", {
|
||||
user: User,
|
||||
history: Vec<Message>,
|
||||
});
|
||||
|
||||
#[derive(Deserialize, Debug, Serialize)]
|
||||
pub enum MessageRole {
|
||||
User,
|
||||
AI,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Serialize)]
|
||||
pub struct Message {
|
||||
conversation_id: String,
|
||||
role: MessageRole,
|
||||
content: String,
|
||||
references: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub struct Conversation {
|
||||
user_id: String,
|
||||
title: String,
|
||||
}
|
||||
|
||||
pub async fn show_initialized_chat(
|
||||
State(state): State<AppState>,
|
||||
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||
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()),
|
||||
};
|
||||
|
||||
info!("{:?}", form);
|
||||
|
||||
let user_message = Message {
|
||||
conversation_id: "test".to_string(),
|
||||
role: MessageRole::User,
|
||||
content: form.user_query,
|
||||
references: None,
|
||||
};
|
||||
|
||||
let ai_message = Message {
|
||||
conversation_id: "test".to_string(),
|
||||
role: MessageRole::AI,
|
||||
content: form.llm_response,
|
||||
references: Some(form.references),
|
||||
};
|
||||
|
||||
let messages = vec![user_message, ai_message];
|
||||
|
||||
let output = render_template(
|
||||
ChatData::template_name(),
|
||||
ChatData {
|
||||
history: messages,
|
||||
user,
|
||||
},
|
||||
state.templates.clone(),
|
||||
)?;
|
||||
|
||||
Ok(output.into_response())
|
||||
}
|
||||
|
||||
pub async fn show_chat_base(
|
||||
State(state): State<AppState>,
|
||||
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
info!("Displaying empty chat start");
|
||||
|
||||
let user = match auth.current_user {
|
||||
Some(user) => user,
|
||||
None => return Ok(Redirect::to("/").into_response()),
|
||||
};
|
||||
|
||||
let output = render_template(
|
||||
ChatData::template_name(),
|
||||
ChatData {
|
||||
history: vec![],
|
||||
user,
|
||||
},
|
||||
state.templates.clone(),
|
||||
)?;
|
||||
|
||||
Ok(output.into_response())
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use crate::error::{HtmlError, IntoHtmlError};
|
||||
|
||||
pub mod account;
|
||||
pub mod admin_panel;
|
||||
pub mod chat;
|
||||
pub mod content;
|
||||
pub mod documentation;
|
||||
pub mod gdpr;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
response::{Html, IntoResponse, Redirect},
|
||||
response::{IntoResponse, Redirect},
|
||||
};
|
||||
use axum_session_auth::AuthSession;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{engine::any::Any, Surreal};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
error::HtmlError, retrieval::query_helper::get_answer_with_references, server::AppState,
|
||||
error::HtmlError,
|
||||
retrieval::query_helper::get_answer_with_references,
|
||||
server::{routes::html::render_template, AppState},
|
||||
storage::types::user::User,
|
||||
};
|
||||
#[derive(Deserialize)]
|
||||
@@ -17,6 +19,13 @@ 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<AppState>,
|
||||
Query(query): Query<SearchParams>,
|
||||
@@ -26,17 +35,37 @@ pub async fn search_result_handler(
|
||||
|
||||
let user = match auth.current_user {
|
||||
Some(user) => user,
|
||||
None => return Ok(Redirect::to("/").into_response()),
|
||||
None => return Ok(Redirect::to("/signin").into_response()),
|
||||
};
|
||||
|
||||
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 = 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()))?;
|
||||
|
||||
Ok(Html(answer.content).into_response())
|
||||
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",
|
||||
AnswerData {
|
||||
user_query: query.query,
|
||||
answer_content: answer,
|
||||
answer_references: references,
|
||||
},
|
||||
state.templates,
|
||||
)?;
|
||||
|
||||
Ok(output.into_response())
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use axum_session_surreal::SessionSurrealPool;
|
||||
use html::{
|
||||
account::{delete_account, set_api_key, show_account_page, update_timezone},
|
||||
admin_panel::{show_admin_panel, toggle_registration_status},
|
||||
chat::{show_chat_base, show_initialized_chat},
|
||||
content::{patch_text_content, show_content_page, show_text_content_edit_form},
|
||||
documentation::{
|
||||
show_documentation_index, show_get_started, show_mobile_friendly, show_privacy_policy,
|
||||
@@ -63,6 +64,7 @@ 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("/signout", get(sign_out_user))
|
||||
.route("/signin", get(show_signin_form).post(authenticate_user))
|
||||
.route(
|
||||
|
||||
44
templates/chat/base.html
Normal file
44
templates/chat/base.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends 'body_base.html' %}
|
||||
{% block main %}
|
||||
<div class="drawer xl:drawer-open">
|
||||
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<!-- Drawer Content -->
|
||||
<div class="drawer-content flex justify-center">
|
||||
<main class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10 max-w-3xl w-full absolute left-0 right-0 mx-auto">
|
||||
<div class="relative w-full">
|
||||
{% include "chat/history.html" %}
|
||||
|
||||
<div class="fixed w-full mx-auto max-w-3xl left-0 right-0 bottom-0">
|
||||
{% include "chat/input_field.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Drawer Sidebar -->
|
||||
{% include "chat/drawer.html" %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Custom styles to override DaisyUI defaults */
|
||||
.drawer-content {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.drawer-side {
|
||||
z-index: 20;
|
||||
/* Ensure drawer is above content */
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
|
||||
/* xl breakpoint */
|
||||
.drawer-open .drawer-content {
|
||||
margin-left: 0;
|
||||
/* Prevent content shift */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
13
templates/chat/drawer.html
Normal file
13
templates/chat/drawer.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="drawer-side z-50">
|
||||
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<ul class="menu bg-base-200 text-base-content min-h-full w-72">
|
||||
<!-- Sidebar content here -->
|
||||
<li class="mt-4 cursor-pointer "><a href="/chat" hx-boost="true" class="flex justify-between">Create new
|
||||
chat<span>{% include
|
||||
"icons/edit_icon.html" %}
|
||||
</span></a></li>
|
||||
<div class="divider"></div>
|
||||
<li><a>Sidebar Item 1</a></li>
|
||||
<li><a>Sidebar Item 2</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
19
templates/chat/history.html
Normal file
19
templates/chat/history.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div id="chat_container">
|
||||
{% for message in history %}
|
||||
{% if message.role == "AI" %}
|
||||
<div class="chat chat-start">
|
||||
<div class="chat-header">{{ message.role }}</div>
|
||||
<div class="chat-bubble">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="chat chat-end">
|
||||
<div class="chat-header">{{ message.role }}</div>
|
||||
<div class="chat-bubble">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
22
templates/chat/input_field.html
Normal file
22
templates/chat/input_field.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<form hx-post="/chat/{{conversation_id}}" hx-target="#chat_container" hx-swap="afterend" class="relative flex gap-2"
|
||||
id="chat-form">
|
||||
<textarea name="content" placeholder="Type your message..." rows="2"
|
||||
class="textarea rounded-t-2xl rounded-b-none border-2 flex-grow resize-none" id="chat-input"></textarea>
|
||||
<button type="submit" class="absolute p-2 cursor-pointer right-0.5 btn-ghost btn-sm top-2">{% include
|
||||
"icons/send_icon.html" %}
|
||||
</button>
|
||||
<label for="my-drawer-2" class="absolute cursor-pointer top-10 right-0.5 p-2 drawer-button xl:hidden z-20 ">
|
||||
{% include "icons/hamburger_icon.html" %}
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('chat-input').addEventListener('keydown', function (e) {
|
||||
// Check if Enter is pressed without Shift
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault(); // Prevent default Enter behavior (new line)
|
||||
document.getElementById('chat-form').submit(); // Submit the form
|
||||
}
|
||||
// Shift + Enter will naturally create a new line due to browser default behavior
|
||||
});
|
||||
</script>
|
||||
@@ -13,7 +13,13 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="card-title truncate">
|
||||
{{ text_content.text }}
|
||||
{% if text_content.url %}
|
||||
<a href="{{ text_content.url}}">{{text_content.url}}</a>
|
||||
{% elif text_content.file_info %}
|
||||
{{text_content.file_info.file_name}}
|
||||
{% else %}
|
||||
{{text_content.text}}
|
||||
{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
|
||||
4
templates/icons/hamburger_icon.html
Normal file
4
templates/icons/hamburger_icon.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 244 B |
5
templates/icons/send_icon.html
Normal file
5
templates/icons/send_icon.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 301 B |
31
templates/index/signed_in/search_response.html
Normal file
31
templates/index/signed_in/search_response.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<div class="mx-auto mb-6">
|
||||
<div class="card bg-base-200 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Result</h2>
|
||||
<div class="prose !max-w-none">
|
||||
{{ answer_content | safe}}
|
||||
</div>
|
||||
{% if answer_references %}
|
||||
<div class="mt-4">
|
||||
<h2 class="card-title mb-2">References</h2>
|
||||
<div class="flex flex-wrap gap-2 max-w-full">
|
||||
{% for ref in answer_references %}
|
||||
<div class="tooltip" data-tip="More info about {{ ref }}">
|
||||
<button class="badge truncate badge-outline cursor-pointer text-gray-500 hover:text-gray-700">
|
||||
{{ ref }}
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
<div class="mt-4">
|
||||
<form hx-post="/chat" hx-target="body" hx-swap="outerHTML" method="POST" class="flex items-center space-x-4">
|
||||
<input type="hidden" name="user_query" value="{{ user_query }}">
|
||||
<input type="hidden" name="llm_response" value="{{ answer_content }}">
|
||||
<input type="hidden" name="references" value="{{ answer_references }}">
|
||||
<button type="submit" class="btn btn-primary">Continue with chat</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
<h2>
|
||||
<h2 class="font-bold mb-2">
|
||||
Search your content
|
||||
</h2>
|
||||
<input type="text" placeholder="Search your knowledge base" class="input input-bordered w-full" name="query"
|
||||
|
||||
@@ -17,12 +17,13 @@ hx-swap="outerHTML"
|
||||
</div>
|
||||
|
||||
<div class="form-control relative" style="margin-top: -1.5rem;">
|
||||
<span class="absolute left-2.5 top-2.5 z-[100] p-0.5 bg-white text-xs text-light">Type</span>
|
||||
<select name="entity_type" class="select select-bordered w-full">
|
||||
<div class="absolute !left-3 !top-2.5 z-50 p-0.5 bg-white text-xs text-light">Type</div>
|
||||
<select name="entity_type" class="select w-full">
|
||||
<option disabled>You must select a type</option>
|
||||
{% for et in entity_types %}
|
||||
<option value="{{ et }}" {% if entity.entity_type==et %}selected{% endif %}>{{ et }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<input type="text" name="id" value="{{ entity.id }}" class="hidden">
|
||||
|
||||
Reference in New Issue
Block a user