From c85cd4556cb64b17b3847ea8cce4a9d2f9919393 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sat, 29 Nov 2025 23:10:03 +0100 Subject: [PATCH] feat: improve chat UX and add optional auth for public feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add debounced search (200ms) in chat sidebar for better performance - Add toast notifications for conversation actions (archive, restore, delete, pin) - Add race condition protection when loading conversations - Add OptionalAuthGuard for public feedback endpoint (unauthenticated access) - Add backHref prop to PageHeader component for back navigation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/lib/components/chat/ChatLayout.svelte | 26 ++++++-- .../src/lib/stores/conversations.svelte.ts | 46 ++++++++------ .../routes/(protected)/chat/[id]/+page.svelte | 61 ++++++++++++++----- .../shared-ui/src/molecules/PageHeader.svelte | 8 +++ .../src/common/guards/optional-auth.guard.ts | 57 +++++++++++++++++ .../src/feedback/feedback.controller.ts | 14 ++++- .../src/feedback/feedback.service.ts | 33 +++++----- 7 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 services/mana-core-auth/src/common/guards/optional-auth.guard.ts diff --git a/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte b/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte index 8b8a31749..192558f3b 100644 --- a/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte +++ b/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte @@ -26,18 +26,36 @@ const MIN_WIDTH = 260; const MAX_WIDTH = 450; - // Search state + // Search state with debouncing let searchQuery = $state(''); + let debouncedSearchQuery = $state(''); + let searchDebounceTimer: ReturnType | null = null; + + // Debounce search input (200ms delay) + $effect(() => { + if (searchDebounceTimer) { + clearTimeout(searchDebounceTimer); + } + searchDebounceTimer = setTimeout(() => { + debouncedSearchQuery = searchQuery; + }, 200); + + return () => { + if (searchDebounceTimer) { + clearTimeout(searchDebounceTimer); + } + }; + }); // Get conversations from store let conversations = $derived(conversationsStore.conversations); let isLoading = $derived(conversationsStore.isLoading); - // Filtered conversations based on search + // Filtered conversations based on debounced search let filteredConversations = $derived( - searchQuery.trim() + debouncedSearchQuery.trim() ? conversations.filter((conv) => - conv.title?.toLowerCase().includes(searchQuery.toLowerCase()) + conv.title?.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) ) : conversations ); diff --git a/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts b/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts index 912f8676b..fe48688d9 100644 --- a/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts +++ b/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts @@ -12,6 +12,17 @@ let archivedConversations = $state([]); let isLoading = $state(false); let error = $state(null); +/** + * Sort conversations: pinned first, then by updatedAt descending + */ +function sortConversations(list: Conversation[]): Conversation[] { + return [...list].sort((a, b) => { + if (a.isPinned && !b.isPinned) return -1; + if (!a.isPinned && b.isPinned) return 1; + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); + }); +} + export const conversationsStore = { // Getters get conversations() { @@ -102,6 +113,9 @@ export const conversationsStore = { conversations = conversations.filter((c) => c.id !== conversationId); archivedConversations = [{ ...conversation, isArchived: true }, ...archivedConversations]; } + toastStore.success('Konversation archiviert'); + } else { + toastStore.error('Konversation konnte nicht archiviert werden'); } return success; @@ -117,8 +131,11 @@ export const conversationsStore = { const conversation = archivedConversations.find((c) => c.id === conversationId); if (conversation) { archivedConversations = archivedConversations.filter((c) => c.id !== conversationId); - conversations = [{ ...conversation, isArchived: false }, ...conversations]; + conversations = sortConversations([{ ...conversation, isArchived: false }, ...conversations]); } + toastStore.success('Konversation wiederhergestellt'); + } else { + toastStore.error('Konversation konnte nicht wiederhergestellt werden'); } return success; @@ -133,6 +150,9 @@ export const conversationsStore = { if (success) { conversations = conversations.filter((c) => c.id !== conversationId); archivedConversations = archivedConversations.filter((c) => c.id !== conversationId); + toastStore.success('Konversation gelöscht'); + } else { + toastStore.error('Konversation konnte nicht gelöscht werden'); } return success; @@ -145,15 +165,11 @@ export const conversationsStore = { const success = await conversationService.pinConversation(conversationId); if (success) { - conversations = conversations.map((c) => - c.id === conversationId ? { ...c, isPinned: true } : c + conversations = sortConversations( + conversations.map((c) => (c.id === conversationId ? { ...c, isPinned: true } : c)) ); - // Re-sort: pinned first, then by updatedAt - conversations = [...conversations].sort((a, b) => { - if (a.isPinned && !b.isPinned) return -1; - if (!a.isPinned && b.isPinned) return 1; - return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); - }); + } else { + toastStore.error('Konversation konnte nicht angepinnt werden'); } return success; @@ -166,15 +182,11 @@ export const conversationsStore = { const success = await conversationService.unpinConversation(conversationId); if (success) { - conversations = conversations.map((c) => - c.id === conversationId ? { ...c, isPinned: false } : c + conversations = sortConversations( + conversations.map((c) => (c.id === conversationId ? { ...c, isPinned: false } : c)) ); - // Re-sort: pinned first, then by updatedAt - conversations = [...conversations].sort((a, b) => { - if (a.isPinned && !b.isPinned) return -1; - if (!a.isPinned && b.isPinned) return 1; - return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); - }); + } else { + toastStore.error('Konversation konnte nicht losgelöst werden'); } return success; diff --git a/apps/chat/apps/web/src/routes/(protected)/chat/[id]/+page.svelte b/apps/chat/apps/web/src/routes/(protected)/chat/[id]/+page.svelte index b5e1e4e80..69a3b5670 100644 --- a/apps/chat/apps/web/src/routes/(protected)/chat/[id]/+page.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/chat/[id]/+page.svelte @@ -1,11 +1,11 @@