mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-24 01:38:29 +02:00
feat: chat conversation list titles, sorting, etc
This commit is contained in:
@@ -224,3 +224,110 @@ pub async fn new_chat_user_message(
|
||||
|
||||
Ok(response.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PatchConversationTitle {
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DrawerContext {
|
||||
user: User,
|
||||
conversation_archive: Vec<Conversation>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
edit_conversation_id: Option<String>,
|
||||
}
|
||||
pub async fn show_conversation_editing_title(
|
||||
State(state): State<HtmlState>,
|
||||
RequireUser(user): RequireUser,
|
||||
Path(conversation_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
||||
|
||||
let owns = conversation_archive
|
||||
.iter()
|
||||
.any(|c| c.id == conversation_id && c.user_id == user.id);
|
||||
if !owns {
|
||||
return Ok(TemplateResponse::unauthorized().into_response());
|
||||
}
|
||||
|
||||
Ok(TemplateResponse::new_template(
|
||||
"chat/drawer.html",
|
||||
DrawerContext {
|
||||
user,
|
||||
conversation_archive,
|
||||
edit_conversation_id: Some(conversation_id),
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn patch_conversation_title(
|
||||
State(state): State<HtmlState>,
|
||||
RequireUser(user): RequireUser,
|
||||
Path(conversation_id): Path<String>,
|
||||
Form(form): Form<PatchConversationTitle>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
Conversation::patch_title(&conversation_id, &user.id, &form.title, &state.db).await?;
|
||||
|
||||
let updated_conversations = User::get_user_conversations(&user.id, &state.db).await?;
|
||||
|
||||
Ok(TemplateResponse::new_template(
|
||||
"chat/drawer.html",
|
||||
DrawerContext {
|
||||
user,
|
||||
conversation_archive: updated_conversations,
|
||||
edit_conversation_id: None,
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn delete_conversation(
|
||||
State(state): State<HtmlState>,
|
||||
RequireUser(user): RequireUser,
|
||||
Path(conversation_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
let conversation: Conversation = state
|
||||
.db
|
||||
.get_item(&conversation_id)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Conversation not found".to_string()))?;
|
||||
|
||||
if conversation.user_id != user.id {
|
||||
return Ok(TemplateResponse::unauthorized().into_response());
|
||||
}
|
||||
|
||||
state
|
||||
.db
|
||||
.delete_item::<Conversation>(&conversation_id)
|
||||
.await?;
|
||||
|
||||
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
||||
|
||||
Ok(TemplateResponse::new_template(
|
||||
"chat/drawer.html",
|
||||
DrawerContext {
|
||||
user,
|
||||
conversation_archive,
|
||||
edit_conversation_id: None,
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
pub async fn reload_sidebar(
|
||||
State(state): State<HtmlState>,
|
||||
RequireUser(user): RequireUser,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
||||
|
||||
Ok(TemplateResponse::new_template(
|
||||
"chat/drawer.html",
|
||||
DrawerContext {
|
||||
user,
|
||||
conversation_archive,
|
||||
edit_conversation_id: None,
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use chat_handlers::{
|
||||
new_chat_user_message, new_user_message, show_chat_base, show_existing_chat,
|
||||
delete_conversation, new_chat_user_message, new_user_message, patch_conversation_title,
|
||||
reload_sidebar, show_chat_base, show_conversation_editing_title, show_existing_chat,
|
||||
show_initialized_chat,
|
||||
};
|
||||
use message_response_stream::get_response_stream;
|
||||
@@ -23,7 +24,17 @@ where
|
||||
{
|
||||
Router::new()
|
||||
.route("/chat", get(show_chat_base).post(new_chat_user_message))
|
||||
.route("/chat/:id", get(show_existing_chat).post(new_user_message))
|
||||
.route(
|
||||
"/chat/:id",
|
||||
get(show_existing_chat)
|
||||
.post(new_user_message)
|
||||
.delete(delete_conversation),
|
||||
)
|
||||
.route(
|
||||
"/chat/:id/title",
|
||||
get(show_conversation_editing_title).patch(patch_conversation_title),
|
||||
)
|
||||
.route("/chat/sidebar", get(reload_sidebar))
|
||||
.route("/initialized-chat", post(show_initialized_chat))
|
||||
.route("/chat/response-stream", get(get_response_stream))
|
||||
.route("/chat/reference/:id", get(show_reference_tooltip))
|
||||
|
||||
@@ -1,15 +1,44 @@
|
||||
<div class="drawer-side z-50 max-h-[calc(100vh-65px)]">
|
||||
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<ul class="menu bg-base-200 text-base-content w-72">
|
||||
<ul class="menu bg-base-200 text-base-content w-72 ">
|
||||
<!-- Sidebar content here -->
|
||||
<li class="mt-4 cursor-pointer "><a href="/chat" hx-boost="true" class="flex justify-between">Create new
|
||||
<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>
|
||||
{% for conversation in conversation_archive %}
|
||||
<li><a href="/chat/{{conversation.id}}" hx-boost="true">{{conversation.title}} - {{conversation.created_at}}</a>
|
||||
{% if edit_conversation_id == conversation.id %}
|
||||
<!-- Render the editable title form variant -->
|
||||
<li class="align-center" id="conversation-{{ conversation.id }}">
|
||||
<form hx-patch="/chat/{{ conversation.id }}/title" hx-target=".drawer-side" hx-swap="outerHTML"
|
||||
class="flex items-center gap-2">
|
||||
<input type="text" name="title" value="{{ conversation.title }}" class="input input-sm" />
|
||||
<button type="submit">{% include "icons/check_icon.html" %}
|
||||
</button>
|
||||
<button type="button" hx-get="/chat/sidebar" hx-target=".drawer-side" hx-swap="outerHTML">{% include
|
||||
"icons/x_icon.html" %}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<!-- Render the normal view mode conversation item -->
|
||||
<li class="align-center" id="conversation-{{ conversation.id }}">
|
||||
<div class="justify-between">
|
||||
<a href="/chat/{{ conversation.id }}" hx-boost="true">
|
||||
{{ conversation.title }} - {{ conversation.created_at | datetimeformat(format="short", tz=user.timezone) }}
|
||||
</a>
|
||||
<div class="flex gap-0.5">
|
||||
<button hx-get="/chat/{{ conversation.id }}/title" hx-target=".drawer-side" hx-swap="outerHTML">
|
||||
{% include "icons/edit_icon.html" %}
|
||||
</button>
|
||||
<button hx-delete="/chat/{{ conversation.id }}" hx-target=".drawer-side" hx-swap="outerHTML">
|
||||
{% include "icons/delete_icon.html" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
4
html-router/templates/icons/check_icon.html
Normal file
4
html-router/templates/icons/check_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="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 221 B |
4
html-router/templates/icons/x_icon.html
Normal file
4
html-router/templates/icons/x_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="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 220 B |
Reference in New Issue
Block a user