feat: chat conversation list titles, sorting, etc

This commit is contained in:
Per Stark
2025-04-15 16:34:44 +02:00
parent fbd534ff39
commit 6950009675
9 changed files with 309 additions and 7 deletions

View File

@@ -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())
}

View File

@@ -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))

View File

@@ -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>

View 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

View 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