mirror of
https://github.com/perstarkse/minne.git
synced 2026-07-05 20:41:41 +02:00
multi chat and history, oob swap
This commit is contained in:
@@ -828,6 +828,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.\!tooltip {
|
||||||
|
position: relative !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
--tt-bg: var(--color-neutral) !important;
|
||||||
|
--tt-off: calc(100% + 0.5rem) !important;
|
||||||
|
--tt-tail: calc(100% + 1px + 0.25rem) !important;
|
||||||
|
> :where(.tooltip-content), &[data-tip]:before {
|
||||||
|
position: absolute !important;
|
||||||
|
max-width: 20rem !important;
|
||||||
|
border-radius: var(--radius-field) !important;
|
||||||
|
padding-inline: calc(0.25rem * 2) !important;
|
||||||
|
padding-block: calc(0.25rem * 1) !important;
|
||||||
|
text-align: center !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
color: var(--color-neutral-content) !important;
|
||||||
|
opacity: 0% !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
line-height: 1.25em !important;
|
||||||
|
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms !important;
|
||||||
|
background-color: var(--tt-bg) !important;
|
||||||
|
width: max-content !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
z-index: 1 !important;
|
||||||
|
--tw-content: attr(data-tip) !important;
|
||||||
|
content: var(--tw-content) !important;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
position: absolute !important;
|
||||||
|
position: absolute !important;
|
||||||
|
opacity: 0% !important;
|
||||||
|
background-color: var(--tt-bg) !important;
|
||||||
|
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms !important;
|
||||||
|
content: "" !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
width: 0.625rem !important;
|
||||||
|
height: 0.25rem !important;
|
||||||
|
display: block !important;
|
||||||
|
mask-repeat: no-repeat !important;
|
||||||
|
mask-position: -1px 0 !important;
|
||||||
|
mask-image: url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A") !important;
|
||||||
|
}
|
||||||
|
&.tooltip-open, &[data-tip]:hover, &:hover, &:has(:focus-visible) {
|
||||||
|
> .tooltip-content, &[data-tip]:before, &:after {
|
||||||
|
opacity: 100% !important;
|
||||||
|
--tt-pos: 0rem !important;
|
||||||
|
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .tooltip-content, &[data-tip]:before {
|
||||||
|
transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)) !important;
|
||||||
|
inset: auto auto var(--tt-off) 50% !important;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)) !important;
|
||||||
|
inset: auto auto var(--tt-tail) 50% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -2544,12 +2601,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.z-5 {
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
.z-10 {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
.z-20 {
|
.z-20 {
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
.z-50 {
|
.z-50 {
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
.z-\[9999\] {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
.modal-box {
|
.modal-box {
|
||||||
grid-column-start: 1;
|
grid-column-start: 1;
|
||||||
grid-row-start: 1;
|
grid-row-start: 1;
|
||||||
@@ -2639,6 +2705,12 @@
|
|||||||
max-width: 96rem;
|
max-width: 96rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.m-0 {
|
||||||
|
margin: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
.m-9 {
|
||||||
|
margin: calc(var(--spacing) * 9);
|
||||||
|
}
|
||||||
.filter {
|
.filter {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -4072,6 +4144,9 @@
|
|||||||
.h-32 {
|
.h-32 {
|
||||||
height: calc(var(--spacing) * 32);
|
height: calc(var(--spacing) * 32);
|
||||||
}
|
}
|
||||||
|
.h-\[calc\(100vh-160px\)\] {
|
||||||
|
height: calc(100vh - 160px);
|
||||||
|
}
|
||||||
.min-h-\[100dvh\] {
|
.min-h-\[100dvh\] {
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
@@ -4126,6 +4201,9 @@
|
|||||||
.max-w-full {
|
.max-w-full {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
.max-w-xs {
|
||||||
|
max-width: var(--container-xs);
|
||||||
|
}
|
||||||
.min-w-72 {
|
.min-w-72 {
|
||||||
min-width: calc(var(--spacing) * 72);
|
min-width: calc(var(--spacing) * 72);
|
||||||
}
|
}
|
||||||
@@ -4320,6 +4398,9 @@
|
|||||||
.overflow-x-auto {
|
.overflow-x-auto {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
.overflow-y-auto {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
.rounded-2xl {
|
.rounded-2xl {
|
||||||
border-radius: var(--radius-2xl);
|
border-radius: var(--radius-2xl);
|
||||||
}
|
}
|
||||||
@@ -4378,6 +4459,9 @@
|
|||||||
.border-gray-200 {
|
.border-gray-200 {
|
||||||
border-color: var(--color-gray-200);
|
border-color: var(--color-gray-200);
|
||||||
}
|
}
|
||||||
|
.border-neutral-700 {
|
||||||
|
border-color: var(--color-neutral-700);
|
||||||
|
}
|
||||||
.bg-accent {
|
.bg-accent {
|
||||||
background-color: var(--color-accent);
|
background-color: var(--color-accent);
|
||||||
}
|
}
|
||||||
@@ -4393,6 +4477,9 @@
|
|||||||
.bg-gray-200 {
|
.bg-gray-200 {
|
||||||
background-color: var(--color-gray-200);
|
background-color: var(--color-gray-200);
|
||||||
}
|
}
|
||||||
|
.bg-neutral-800 {
|
||||||
|
background-color: var(--color-neutral-800);
|
||||||
|
}
|
||||||
.bg-secondary {
|
.bg-secondary {
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
@@ -4429,12 +4516,18 @@
|
|||||||
.p-2 {
|
.p-2 {
|
||||||
padding: calc(var(--spacing) * 2);
|
padding: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.p-3 {
|
||||||
|
padding: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
.p-4 {
|
.p-4 {
|
||||||
padding: calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.p-5 {
|
.p-5 {
|
||||||
padding: calc(var(--spacing) * 5);
|
padding: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.p-7 {
|
||||||
|
padding: calc(var(--spacing) * 7);
|
||||||
|
}
|
||||||
.menu-title {
|
.menu-title {
|
||||||
padding-inline: calc(0.25rem * 3);
|
padding-inline: calc(0.25rem * 3);
|
||||||
padding-block: calc(0.25rem * 2);
|
padding-block: calc(0.25rem * 2);
|
||||||
@@ -4506,6 +4599,18 @@
|
|||||||
.pb-10 {
|
.pb-10 {
|
||||||
padding-bottom: calc(var(--spacing) * 10);
|
padding-bottom: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
|
.pb-32 {
|
||||||
|
padding-bottom: calc(var(--spacing) * 32);
|
||||||
|
}
|
||||||
|
.pl-2 {
|
||||||
|
padding-left: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.pl-3 {
|
||||||
|
padding-left: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.pl-4 {
|
||||||
|
padding-left: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -4646,6 +4751,9 @@
|
|||||||
.text-transparent {
|
.text-transparent {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
.text-white {
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
.lowercase {
|
.lowercase {
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
@@ -4672,6 +4780,9 @@
|
|||||||
.underline {
|
.underline {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
|
.opacity-0 {
|
||||||
|
opacity: 0%;
|
||||||
|
}
|
||||||
.opacity-50 {
|
.opacity-50 {
|
||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
}
|
}
|
||||||
@@ -4693,6 +4804,10 @@
|
|||||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
.shadow-lg {
|
||||||
|
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
.shadow-md {
|
.shadow-md {
|
||||||
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
@@ -4790,6 +4905,13 @@
|
|||||||
--tw-prose-td-borders: color-mix(in oklab, var(--color-base-content) 20%, transparent);
|
--tw-prose-td-borders: color-mix(in oklab, var(--color-base-content) 20%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:bg-gray-200 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-gray-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:text-blue-700 {
|
.hover\:text-blue-700 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -5128,6 +5250,15 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:hover\:bg-gray-700 {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.prose-h1\:mb-2 {
|
.prose-h1\:mb-2 {
|
||||||
& :is(:where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *))) {
|
& :is(:where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *))) {
|
||||||
margin-bottom: calc(var(--spacing) * 2);
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod message_response_stream;
|
pub mod message_response_stream;
|
||||||
|
pub mod references;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
@@ -16,7 +17,7 @@ use crate::{
|
|||||||
page_data,
|
page_data,
|
||||||
server::{routes::html::render_template, AppState},
|
server::{routes::html::render_template, AppState},
|
||||||
storage::{
|
storage::{
|
||||||
db::store_item,
|
db::{get_item, store_item},
|
||||||
types::{
|
types::{
|
||||||
conversation::Conversation,
|
conversation::Conversation,
|
||||||
message::{Message, MessageRole},
|
message::{Message, MessageRole},
|
||||||
@@ -46,7 +47,7 @@ where
|
|||||||
page_data!(ChatData, "chat/base.html", {
|
page_data!(ChatData, "chat/base.html", {
|
||||||
user: User,
|
user: User,
|
||||||
history: Vec<Message>,
|
history: Vec<Message>,
|
||||||
conversation: Conversation,
|
conversation: Option<Conversation>,
|
||||||
conversation_archive: Vec<Conversation>
|
conversation_archive: Vec<Conversation>
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ pub async fn show_initialized_chat(
|
|||||||
history: messages,
|
history: messages,
|
||||||
user,
|
user,
|
||||||
conversation_archive,
|
conversation_archive,
|
||||||
conversation: conversation.clone(),
|
conversation: Some(conversation.clone()),
|
||||||
},
|
},
|
||||||
state.templates.clone(),
|
state.templates.clone(),
|
||||||
)?;
|
)?;
|
||||||
@@ -129,15 +130,13 @@ pub async fn show_chat_base(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
|
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
|
||||||
|
|
||||||
let conversation = Conversation::new(user.id.clone(), "New Chat".to_string());
|
|
||||||
|
|
||||||
let output = render_template(
|
let output = render_template(
|
||||||
ChatData::template_name(),
|
ChatData::template_name(),
|
||||||
ChatData {
|
ChatData {
|
||||||
history: vec![],
|
history: vec![],
|
||||||
user,
|
user,
|
||||||
conversation_archive,
|
conversation_archive,
|
||||||
conversation,
|
conversation: None,
|
||||||
},
|
},
|
||||||
state.templates.clone(),
|
state.templates.clone(),
|
||||||
)?;
|
)?;
|
||||||
@@ -179,7 +178,7 @@ pub async fn show_existing_chat(
|
|||||||
ChatData {
|
ChatData {
|
||||||
history: messages,
|
history: messages,
|
||||||
user,
|
user,
|
||||||
conversation: conversation.clone(),
|
conversation: Some(conversation.clone()),
|
||||||
conversation_archive,
|
conversation_archive,
|
||||||
},
|
},
|
||||||
state.templates.clone(),
|
state.templates.clone(),
|
||||||
@@ -194,11 +193,28 @@ pub async fn new_user_message(
|
|||||||
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||||
Form(form): Form<NewMessageForm>,
|
Form(form): Form<NewMessageForm>,
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
let _user = match auth.current_user {
|
let user = match auth.current_user {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => return Ok(Redirect::to("/").into_response()),
|
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);
|
let user_message = Message::new(conversation_id, MessageRole::User, form.content, None);
|
||||||
|
|
||||||
store_item(&state.surreal_db_client, user_message.clone())
|
store_item(&state.surreal_db_client, user_message.clone())
|
||||||
@@ -216,5 +232,58 @@ pub async fn new_user_message(
|
|||||||
state.templates.clone(),
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -17,8 +17,9 @@ use html::{
|
|||||||
account::{delete_account, set_api_key, show_account_page, update_timezone},
|
account::{delete_account, set_api_key, show_account_page, update_timezone},
|
||||||
admin_panel::{show_admin_panel, toggle_registration_status},
|
admin_panel::{show_admin_panel, toggle_registration_status},
|
||||||
chat::{
|
chat::{
|
||||||
message_response_stream::get_response_stream, new_user_message, show_chat_base,
|
message_response_stream::get_response_stream, new_chat_user_message, new_user_message,
|
||||||
show_existing_chat, show_initialized_chat,
|
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},
|
content::{patch_text_content, show_content_page, show_text_content_edit_form},
|
||||||
documentation::{
|
documentation::{
|
||||||
@@ -67,9 +68,11 @@ pub fn html_routes(app_state: &AppState) -> Router<AppState> {
|
|||||||
.route("/gdpr/accept", post(accept_gdpr))
|
.route("/gdpr/accept", post(accept_gdpr))
|
||||||
.route("/gdpr/deny", post(deny_gdpr))
|
.route("/gdpr/deny", post(deny_gdpr))
|
||||||
.route("/search", get(search_result_handler))
|
.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/:id", get(show_existing_chat).post(new_user_message))
|
||||||
.route("/chat/response-stream", get(get_response_stream))
|
.route("/chat/response-stream", get(get_response_stream))
|
||||||
|
.route("/knowledge/:id", get(show_reference_tooltip))
|
||||||
.route("/signout", get(sign_out_user))
|
.route("/signout", get(sign_out_user))
|
||||||
.route("/signin", get(show_signin_form).post(authenticate_user))
|
.route("/signin", get(show_signin_form).post(authenticate_user))
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -34,4 +34,21 @@ impl Message {
|
|||||||
references,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-17
@@ -1,15 +1,18 @@
|
|||||||
<div id="chat_container">
|
<div id="chat_container" class="pl-3 overflow-y-auto h-[calc(100vh-160px)] pb-32">
|
||||||
{% for message in history %}
|
{% for message in history %}
|
||||||
{% if message.role == "AI" %}
|
{% if message.role == "AI" %}
|
||||||
<div class="chat chat-start">
|
<div class="chat chat-start">
|
||||||
<div class="chat-header">{{ message.role }}</div>
|
<div>
|
||||||
<div class="chat-bubble">
|
<div class="chat-bubble">
|
||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
|
</div>
|
||||||
|
{% if message.references %}
|
||||||
|
{% include "chat/reference_list.html" %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="chat chat-end">
|
<div class="chat chat-end">
|
||||||
<div class="chat-header">{{ message.role }}</div>
|
|
||||||
<div class="chat-bubble">
|
<div class="chat-bubble">
|
||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
</div>
|
</div>
|
||||||
@@ -19,21 +22,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Scroll to latest message after HTMX swap
|
|
||||||
document.body.addEventListener('htmx:afterSwap', function (evt) {
|
document.body.addEventListener('htmx:afterSwap', function (evt) {
|
||||||
|
const chatContainer = document.getElementById('chat_container');
|
||||||
|
if (chatContainer) {
|
||||||
|
setTimeout(() => {
|
||||||
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also scroll when page loads
|
||||||
|
window.addEventListener('load', function () {
|
||||||
const chatContainer = document.getElementById('chat_container');
|
const chatContainer = document.getElementById('chat_container');
|
||||||
if (chatContainer) {
|
if (chatContainer) {
|
||||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
#chat_container {
|
|
||||||
max-height: 70vh;
|
|
||||||
/* Adjust as needed */
|
|
||||||
overflow-y: auto;
|
|
||||||
/* Enable scrolling */
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{% include "chat/streaming_response.html" %}
|
||||||
|
|
||||||
|
<!-- OOB swap targeting the form element directly -->
|
||||||
|
<form id="chat-form" hx-post="/chat/{{conversation.id}}" hx-target="#chat_container" hx-swap="beforeend"
|
||||||
|
class="relative flex gap-2" hx-swap-oob="true">
|
||||||
|
<textarea autofocus required name="content" placeholder="Type your message..." rows="2"
|
||||||
|
class="textarea textarea-ghost rounded-2xl rounded-b-none h-24 sm:rounded-b-2xl pr-8 bg-base-200 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-1">
|
||||||
|
{% include "icons/send_icon.html" %}
|
||||||
|
</button>
|
||||||
|
<label for="my-drawer-2" class="absolute cursor-pointer top-9 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) {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
htmx.trigger('#chat-form', 'submit');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Clear textarea after successful submission
|
||||||
|
document.getElementById('chat-form').addEventListener('htmx:afterRequest', function (e) {
|
||||||
|
if (e.detail.successful) { // Check if the request was successful
|
||||||
|
document.getElementById('chat-input').value = ''; // Clear the textarea
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="fixed w-full mx-auto max-w-3xl p-4 pb-0 sm:pb-4 left-0 right-0 bottom-0">
|
<div class="fixed w-full mx-auto max-w-3xl p-4 pb-0 sm:pb-4 left-0 right-0 bottom-0 bg-base-100 z-10">
|
||||||
<form hx-post="/chat/{{conversation.id}}" hx-target="#chat_container" hx-swap="beforeend" class="relative flex gap-2"
|
<form hx-post="{% if conversation %} /chat/{{conversation.id}} {% else %} /chat {% endif %}"
|
||||||
id="chat-form">
|
hx-target="#chat_container" hx-swap="beforeend" class="relative flex gap-2" id="chat-form">
|
||||||
<textarea autofocus required name="content" placeholder="Type your message..." rows="2"
|
<textarea autofocus required name="content" placeholder="Type your message..." rows="2"
|
||||||
class="textarea textarea-ghost rounded-2xl rounded-b-none h-24 sm:rounded-b-2xl pr-8 bg-base-200 flex-grow resize-none"
|
class="textarea textarea-ghost rounded-2xl rounded-b-none h-24 sm:rounded-b-2xl pr-8 bg-base-200 flex-grow resize-none"
|
||||||
id="chat-input"></textarea>
|
id="chat-input"></textarea>
|
||||||
|
|||||||
@@ -5,21 +5,19 @@
|
|||||||
{% include "icons/chevron_icon.html" %}
|
{% include "icons/chevron_icon.html" %}
|
||||||
</button>
|
</button>
|
||||||
<div id="references-content-{{user_message_id}}" class="hidden max-w-full mt-1">
|
<div id="references-content-{{user_message_id}}" class="hidden max-w-full mt-1">
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap gap-1">
|
||||||
{% for reference in references %}
|
{% for reference in references %}
|
||||||
<span class="badge badge-sm text-xs truncate max-w-[20ch] overflow-hidden text-left block tooltip"
|
<div class="reference-badge-container" data-reference="{{reference}}" data-message-id="{{user_message_id}}"
|
||||||
hx-get="/knowledge/{{reference}}" hx-trigger="mouseenter once delay:500ms"
|
data-index="{{loop.index}}">
|
||||||
hx-target="#tooltip-content-{{loop.index}}-{{user_message_id}}" hx-swap="innerHTML">
|
<span class="badge badge-xs badge-neutral truncate max-w-[20ch] overflow-hidden text-left block cursor-pointer">
|
||||||
{{reference}}
|
{{reference}}
|
||||||
<div id="tooltip-content-{{loop.index}}-{{user_message_id}}" class="tooltip-content">
|
</span>
|
||||||
<!-- Loading indicator while content is fetched -->
|
</div>
|
||||||
<div class="animate-pulse text-gray-400 text-xs">Loading...</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('references-toggle-{{user_message_id}}').addEventListener('click', function () {
|
document.getElementById('references-toggle-{{user_message_id}}').addEventListener('click', function () {
|
||||||
const content = document.getElementById('references-content-{{user_message_id}}');
|
const content = document.getElementById('references-content-{{user_message_id}}');
|
||||||
@@ -27,4 +25,66 @@
|
|||||||
content.classList.toggle('hidden');
|
content.classList.toggle('hidden');
|
||||||
icon.classList.toggle('rotate-180');
|
icon.classList.toggle('rotate-180');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize portal tooltips
|
||||||
|
document.querySelectorAll('.reference-badge-container').forEach(container => {
|
||||||
|
const reference = container.dataset.reference;
|
||||||
|
const messageId = container.dataset.messageId;
|
||||||
|
const index = container.dataset.index;
|
||||||
|
let tooltipId = `tooltip-${messageId}-${index}`;
|
||||||
|
let tooltipContent = null;
|
||||||
|
let tooltipTimeout;
|
||||||
|
|
||||||
|
// Create tooltip element (initially hidden)
|
||||||
|
function createTooltip() {
|
||||||
|
const tooltip = document.createElement('div');
|
||||||
|
tooltip.id = tooltipId;
|
||||||
|
tooltip.className = 'fixed z-[9999] bg-neutral-800 text-white p-3 rounded-md shadow-lg text-sm w-72 max-w-xs border border-neutral-700 hidden';
|
||||||
|
tooltip.innerHTML = '<div class="animate-pulse">Loading...</div>';
|
||||||
|
document.body.appendChild(tooltip);
|
||||||
|
return tooltip;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addEventListener('mouseenter', function () {
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (tooltipTimeout) clearTimeout(tooltipTimeout);
|
||||||
|
|
||||||
|
// Get or create tooltip
|
||||||
|
let tooltip = document.getElementById(tooltipId);
|
||||||
|
if (!tooltip) tooltip = createTooltip();
|
||||||
|
|
||||||
|
// Position tooltip
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`;
|
||||||
|
tooltip.style.left = `${rect.left + window.scrollX}px`;
|
||||||
|
|
||||||
|
// Adjust position if it would overflow viewport
|
||||||
|
const tooltipRect = tooltip.getBoundingClientRect();
|
||||||
|
if (rect.left + tooltipRect.width > window.innerWidth - 20) {
|
||||||
|
tooltip.style.left = `${window.innerWidth - tooltipRect.width - 20 + window.scrollX}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show tooltip
|
||||||
|
tooltip.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Load content if needed
|
||||||
|
if (!tooltipContent) {
|
||||||
|
fetch(`/knowledge/${encodeURIComponent(reference)}`)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
tooltipContent = html;
|
||||||
|
if (document.getElementById(tooltipId)) {
|
||||||
|
document.getElementById(tooltipId).innerHTML = html;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('mouseleave', function () {
|
||||||
|
tooltipTimeout = setTimeout(() => {
|
||||||
|
const tooltip = document.getElementById(tooltipId);
|
||||||
|
if (tooltip) tooltip.classList.add('hidden');
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div>{{entity.name}}</div>
|
||||||
|
<div>{{entity.description}}</div>
|
||||||
|
<div>{{entity.updated_at|datetimeformat(format="short", tz=user.timezone)}} </div>
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>{% endif %}
|
</div>{% endif %}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<form hx-post="/chat" hx-target="body" hx-swap="outerHTML" method="POST" class="flex items-center space-x-4">
|
<form hx-post="/initialized-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="user_query" value="{{ user_query }}">
|
||||||
<input type="hidden" name="llm_response" value="{{ answer_content }}">
|
<input type="hidden" name="llm_response" value="{{ answer_content }}">
|
||||||
<input type="hidden" name="references" value="{{ answer_references }}">
|
<input type="hidden" name="references" value="{{ answer_references }}">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<nav class="navbar bg-base-200 !p-0">
|
<nav class="navbar bg-base-200 !p-0">
|
||||||
<div class="container flex mx-auto">
|
<div class="container flex mx-auto">
|
||||||
<div class="flex-1 flex items-center">
|
<div class="flex-1 flex items-center">
|
||||||
<a class="text-2xl text-primary font-bold" href="/" hx-boost="true">Minne</a>
|
<a class="text-2xl p-2 text-primary font-bold" href="/" hx-boost="true">Minne</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ul class="menu menu-horizontal px-1">
|
<ul class="menu menu-horizontal px-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user