]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
Conversation action dialogs as singletons from Chat Sidebar + apply conditional rende...
authorAleksander Grygier <redacted>
Wed, 1 Oct 2025 16:18:10 +0000 (18:18 +0200)
committerGitHub <redacted>
Wed, 1 Oct 2025 16:18:10 +0000 (18:18 +0200)
* fix: Render Conversation action dialogs as singletons from Chat Sidebar level

* chore: update webui build output

* fix: Render Actions Dropdown conditionally only when user hovers conversation item + remove unused markup

* chore: Update webui static build

* fix: Always truncate conversation names

* chore: Update webui static build

tools/server/public/index.html.gz
tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte
tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebarConversationItem.svelte
tools/server/webui/src/routes/+layout.svelte

index cd3528d3b2e4974bf5f15d52cebcb38372c9cd56..4f18a634ce5454acc56570aa0218cd703707bc10 100644 (file)
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
index 8dd4b20dcbb97a9ebe96a845333c492825e7ebf5..5976e5dd03d7b6c65b245436c76301089da058d8 100644 (file)
@@ -1,9 +1,12 @@
 <script lang="ts">
        import { goto } from '$app/navigation';
        import { page } from '$app/state';
-       import { ChatSidebarConversationItem } from '$lib/components/app';
+       import { Trash2 } from '@lucide/svelte';
+       import { ChatSidebarConversationItem, ConfirmationDialog } from '$lib/components/app';
        import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
        import * as Sidebar from '$lib/components/ui/sidebar';
+       import * as AlertDialog from '$lib/components/ui/alert-dialog';
+       import Input from '$lib/components/ui/input/input.svelte';
        import {
                conversations,
                deleteConversation,
        let currentChatId = $derived(page.params.id);
        let isSearchModeActive = $state(false);
        let searchQuery = $state('');
+       let showDeleteDialog = $state(false);
+       let showEditDialog = $state(false);
+       let selectedConversation = $state<DatabaseConversation | null>(null);
+       let editedName = $state('');
 
        let filteredConversations = $derived.by(() => {
                if (searchQuery.trim().length > 0) {
                return conversations();
        });
 
-       async function editConversation(id: string, name: string) {
-               await updateConversationName(id, name);
+       async function handleDeleteConversation(id: string) {
+               const conversation = conversations().find((conv) => conv.id === id);
+               if (conversation) {
+                       selectedConversation = conversation;
+                       showDeleteDialog = true;
+               }
        }
 
-       async function handleDeleteConversation(id: string) {
-               await deleteConversation(id);
+       async function handleEditConversation(id: string) {
+               const conversation = conversations().find((conv) => conv.id === id);
+               if (conversation) {
+                       selectedConversation = conversation;
+                       editedName = conversation.name;
+                       showEditDialog = true;
+               }
+       }
+
+       function handleConfirmDelete() {
+               if (selectedConversation) {
+                       showDeleteDialog = false;
+
+                       setTimeout(() => {
+                               deleteConversation(selectedConversation.id);
+                               selectedConversation = null;
+                       }, 100); // Wait for animation to finish
+               }
+       }
+
+       function handleConfirmEdit() {
+               if (!editedName.trim() || !selectedConversation) return;
+
+               showEditDialog = false;
+
+               updateConversationName(selectedConversation.id, editedName);
+               selectedConversation = null;
        }
 
        export function handleMobileSidebarItemClick() {
                                                        {handleMobileSidebarItemClick}
                                                        isActive={currentChatId === conversation.id}
                                                        onSelect={selectConversation}
-                                                       onEdit={editConversation}
+                                                       onEdit={handleEditConversation}
                                                        onDelete={handleDeleteConversation}
                                                />
                                        </Sidebar.MenuItem>
                </Sidebar.GroupContent>
        </Sidebar.Group>
 
-       <div class="bottom-0 z-10 bg-sidebar bg-sidebar/50 px-4 py-4 backdrop-blur-lg md:sticky">
-               <p class="text-xs text-muted-foreground">Conversations are stored locally in your browser.</p>
-       </div>
+       <div class="bottom-0 z-10 bg-sidebar bg-sidebar/50 px-4 py-4 backdrop-blur-lg md:sticky"></div>
 </ScrollArea>
+
+<ConfirmationDialog
+       bind:open={showDeleteDialog}
+       title="Delete Conversation"
+       description={selectedConversation
+               ? `Are you sure you want to delete "${selectedConversation.name}"? This action cannot be undone and will permanently remove all messages in this conversation.`
+               : ''}
+       confirmText="Delete"
+       cancelText="Cancel"
+       variant="destructive"
+       icon={Trash2}
+       onConfirm={handleConfirmDelete}
+       onCancel={() => {
+               showDeleteDialog = false;
+               selectedConversation = null;
+       }}
+/>
+
+<AlertDialog.Root bind:open={showEditDialog}>
+       <AlertDialog.Content>
+               <AlertDialog.Header>
+                       <AlertDialog.Title>Edit Conversation Name</AlertDialog.Title>
+                       <AlertDialog.Description>
+                               <Input
+                                       class="mt-4 text-foreground"
+                                       onkeydown={(e) => {
+                                               if (e.key === 'Enter') {
+                                                       e.preventDefault();
+                                                       handleConfirmEdit();
+                                               }
+                                       }}
+                                       placeholder="Enter a new name"
+                                       type="text"
+                                       bind:value={editedName}
+                               />
+                       </AlertDialog.Description>
+               </AlertDialog.Header>
+               <AlertDialog.Footer>
+                       <AlertDialog.Cancel
+                               onclick={() => {
+                                       showEditDialog = false;
+                                       selectedConversation = null;
+                               }}>Cancel</AlertDialog.Cancel
+                       >
+                       <AlertDialog.Action onclick={handleConfirmEdit}>Save</AlertDialog.Action>
+               </AlertDialog.Footer>
+       </AlertDialog.Content>
+</AlertDialog.Root>
index 6c3fb5764eb95f6e7908af6f30bd92948c592435..579d00aabc14adc313a56d374123d9c119a1c93a 100644 (file)
@@ -1,8 +1,6 @@
 <script lang="ts">
        import { Trash2, Pencil, MoreHorizontal } from '@lucide/svelte';
-       import { ActionDropdown, ConfirmationDialog } from '$lib/components/app';
-       import * as AlertDialog from '$lib/components/ui/alert-dialog';
-       import Input from '$lib/components/ui/input/input.svelte';
+       import { ActionDropdown } from '$lib/components/app';
        import { onMount } from 'svelte';
 
        interface Props {
@@ -10,9 +8,8 @@
                conversation: DatabaseConversation;
                handleMobileSidebarItemClick?: () => void;
                onDelete?: (id: string) => void;
-               onEdit?: (id: string, name: string) => void;
+               onEdit?: (id: string) => void;
                onSelect?: (id: string) => void;
-               showLastModified?: boolean;
        }
 
        let {
                onDelete,
                onEdit,
                onSelect,
-               isActive = false,
-               showLastModified = false
+               isActive = false
        }: Props = $props();
 
-       let editedName = $state('');
-       let showDeleteDialog = $state(false);
-       let showDropdown = $state(false);
-       let showEditDialog = $state(false);
-
-       function formatLastModified(timestamp: number) {
-               const now = Date.now();
-               const diff = now - timestamp;
-               const minutes = Math.floor(diff / (1000 * 60));
-               const hours = Math.floor(diff / (1000 * 60 * 60));
-               const days = Math.floor(diff / (1000 * 60 * 60 * 24));
-
-               if (minutes < 1) return 'Just now';
-               if (minutes < 60) return `${minutes}m ago`;
-               if (hours < 24) return `${hours}h ago`;
-               return `${days}d ago`;
+       let renderActionsDropdown = $state(false);
+       let dropdownOpen = $state(false);
+
+       function handleEdit(event: Event) {
+               event.stopPropagation();
+               onEdit?.(conversation.id);
        }
 
-       function handleConfirmDelete() {
+       function handleDelete(event: Event) {
+               event.stopPropagation();
                onDelete?.(conversation.id);
        }
 
-       function handleConfirmEdit() {
-               if (!editedName.trim()) return;
-               showEditDialog = false;
-               onEdit?.(conversation.id, editedName);
+       function handleGlobalEditEvent(event: Event) {
+               const customEvent = event as CustomEvent<{ conversationId: string }>;
+               if (customEvent.detail.conversationId === conversation.id && isActive) {
+                       handleEdit(event);
+               }
        }
 
-       function handleEdit(event: Event) {
-               event.stopPropagation();
-               editedName = conversation.name;
-               showEditDialog = true;
+       function handleMouseLeave() {
+               if (!dropdownOpen) {
+                       renderActionsDropdown = false;
+               }
+       }
+
+       function handleMouseOver() {
+               renderActionsDropdown = true;
        }
 
        function handleSelect() {
                onSelect?.(conversation.id);
        }
 
-       function handleGlobalEditEvent(event: Event) {
-               const customEvent = event as CustomEvent<{ conversationId: string }>;
-               if (customEvent.detail.conversationId === conversation.id && isActive) {
-                       handleEdit(event);
+       $effect(() => {
+               if (!dropdownOpen) {
+                       renderActionsDropdown = false;
                }
-       }
+       });
 
        onMount(() => {
                document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
        });
 </script>
 
+<!-- svelte-ignore a11y_mouse_events_have_key_events -->
 <button
-       class="group flex w-full cursor-pointer items-center justify-between space-x-3 rounded-lg px-3 py-1.5 text-left transition-colors hover:bg-foreground/10 {isActive
+       class="group flex min-h-9 w-full cursor-pointer items-center justify-between space-x-3 rounded-lg px-3 py-1.5 text-left transition-colors hover:bg-foreground/10 {isActive
                ? 'bg-foreground/5 text-accent-foreground'
                : ''}"
        onclick={handleSelect}
+       onmouseover={handleMouseOver}
+       onmouseleave={handleMouseLeave}
 >
        <!-- svelte-ignore a11y_click_events_have_key_events -->
        <!-- svelte-ignore a11y_no_static_element_interactions -->
-       <div
-               class="text flex min-w-0 flex-1 items-center space-x-3"
-               onclick={handleMobileSidebarItemClick}
-       >
-               <div class="min-w-0 flex-1">
-                       <p class="truncate text-sm font-medium">{conversation.name}</p>
-
-                       {#if showLastModified}
-                               <div class="mt-2 flex flex-wrap items-center space-y-2 space-x-2">
-                                       <span class="w-full text-xs text-muted-foreground">
-                                               {formatLastModified(conversation.lastModified)}
-                                       </span>
-                               </div>
-                       {/if}
-               </div>
-       </div>
-
-       <div class="actions flex items-center">
-               <ActionDropdown
-                       triggerIcon={MoreHorizontal}
-                       triggerTooltip="More actions"
-                       bind:open={showDropdown}
-                       actions={[
-                               {
-                                       icon: Pencil,
-                                       label: 'Edit',
-                                       onclick: handleEdit,
-                                       shortcut: ['shift', 'cmd', 'e']
-                               },
-                               {
-                                       icon: Trash2,
-                                       label: 'Delete',
-                                       onclick: (e) => {
-                                               e.stopPropagation();
-                                               showDeleteDialog = true;
+       <span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}>
+               {conversation.name}
+       </span>
+
+       {#if renderActionsDropdown}
+               <div class="actions flex items-center">
+                       <ActionDropdown
+                               triggerIcon={MoreHorizontal}
+                               triggerTooltip="More actions"
+                               bind:open={dropdownOpen}
+                               actions={[
+                                       {
+                                               icon: Pencil,
+                                               label: 'Edit',
+                                               onclick: handleEdit,
+                                               shortcut: ['shift', 'cmd', 'e']
                                        },
-                                       variant: 'destructive',
-                                       shortcut: ['shift', 'cmd', 'd'],
-                                       separator: true
-                               }
-                       ]}
-               />
-
-               <ConfirmationDialog
-                       bind:open={showDeleteDialog}
-                       title="Delete Conversation"
-                       description={`Are you sure you want to delete "${conversation.name}"? This action cannot be undone and will permanently remove all messages in this conversation.`}
-                       confirmText="Delete"
-                       cancelText="Cancel"
-                       variant="destructive"
-                       icon={Trash2}
-                       onConfirm={handleConfirmDelete}
-                       onCancel={() => (showDeleteDialog = false)}
-               />
-
-               <AlertDialog.Root bind:open={showEditDialog}>
-                       <AlertDialog.Content>
-                               <AlertDialog.Header>
-                                       <AlertDialog.Title>Edit Conversation Name</AlertDialog.Title>
-
-                                       <AlertDialog.Description>
-                                               <Input
-                                                       class="mt-4 text-foreground"
-                                                       onkeydown={(e) => {
-                                                               if (e.key === 'Enter') {
-                                                                       e.preventDefault();
-                                                                       handleConfirmEdit();
-                                                                       showEditDialog = false;
-                                                               }
-                                                       }}
-                                                       placeholder="Enter a new name"
-                                                       type="text"
-                                                       bind:value={editedName}
-                                               />
-                                       </AlertDialog.Description>
-                               </AlertDialog.Header>
-
-                               <AlertDialog.Footer>
-                                       <AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
-
-                                       <AlertDialog.Action onclick={handleConfirmEdit}>Save</AlertDialog.Action>
-                               </AlertDialog.Footer>
-                       </AlertDialog.Content>
-               </AlertDialog.Root>
-       </div>
+                                       {
+                                               icon: Trash2,
+                                               label: 'Delete',
+                                               onclick: handleDelete,
+                                               variant: 'destructive',
+                                               shortcut: ['shift', 'cmd', 'd'],
+                                               separator: true
+                                       }
+                               ]}
+                       />
+               </div>
+       {/if}
 </button>
 
 <style>
index 6fbee0fe355006bbf3d1b26bd9e010e1560234b2..a4555ed5129968e7ff852d33bc15d9e76ff8907a 100644 (file)
        });
 </script>
 
+<svelte:window onkeydown={handleKeydown} />
+
 <ModeWatcher />
 
 <Toaster richColors />
                </Sidebar.Inset>
        </div>
 </Sidebar.Provider>
-
-<svelte:window onkeydown={handleKeydown} />