Skip to content

Commit 7647992

Browse files
authored
Conversation action dialogs as singletons from Chat Sidebar + apply conditional rendering for Actions Dropdown for Chat Conversation Items (ggml-org#16369)
* 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
1 parent 2a9b633 commit 7647992

File tree

4 files changed

+153
-133
lines changed

4 files changed

+153
-133
lines changed

tools/server/public/index.html.gz

-275 Bytes
Binary file not shown.

tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
<script lang="ts">
22
import { goto } from '$app/navigation';
33
import { page } from '$app/state';
4-
import { ChatSidebarConversationItem } from '$lib/components/app';
4+
import { Trash2 } from '@lucide/svelte';
5+
import { ChatSidebarConversationItem, ConfirmationDialog } from '$lib/components/app';
56
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
67
import * as Sidebar from '$lib/components/ui/sidebar';
8+
import * as AlertDialog from '$lib/components/ui/alert-dialog';
9+
import Input from '$lib/components/ui/input/input.svelte';
710
import {
811
conversations,
912
deleteConversation,
@@ -16,6 +19,10 @@
1619
let currentChatId = $derived(page.params.id);
1720
let isSearchModeActive = $state(false);
1821
let searchQuery = $state('');
22+
let showDeleteDialog = $state(false);
23+
let showEditDialog = $state(false);
24+
let selectedConversation = $state<DatabaseConversation | null>(null);
25+
let editedName = $state('');
1926
2027
let filteredConversations = $derived.by(() => {
2128
if (searchQuery.trim().length > 0) {
@@ -27,12 +34,41 @@
2734
return conversations();
2835
});
2936
30-
async function editConversation(id: string, name: string) {
31-
await updateConversationName(id, name);
37+
async function handleDeleteConversation(id: string) {
38+
const conversation = conversations().find((conv) => conv.id === id);
39+
if (conversation) {
40+
selectedConversation = conversation;
41+
showDeleteDialog = true;
42+
}
3243
}
3344
34-
async function handleDeleteConversation(id: string) {
35-
await deleteConversation(id);
45+
async function handleEditConversation(id: string) {
46+
const conversation = conversations().find((conv) => conv.id === id);
47+
if (conversation) {
48+
selectedConversation = conversation;
49+
editedName = conversation.name;
50+
showEditDialog = true;
51+
}
52+
}
53+
54+
function handleConfirmDelete() {
55+
if (selectedConversation) {
56+
showDeleteDialog = false;
57+
58+
setTimeout(() => {
59+
deleteConversation(selectedConversation.id);
60+
selectedConversation = null;
61+
}, 100); // Wait for animation to finish
62+
}
63+
}
64+
65+
function handleConfirmEdit() {
66+
if (!editedName.trim() || !selectedConversation) return;
67+
68+
showEditDialog = false;
69+
70+
updateConversationName(selectedConversation.id, editedName);
71+
selectedConversation = null;
3672
}
3773
3874
export function handleMobileSidebarItemClick() {
@@ -98,7 +134,7 @@
98134
{handleMobileSidebarItemClick}
99135
isActive={currentChatId === conversation.id}
100136
onSelect={selectConversation}
101-
onEdit={editConversation}
137+
onEdit={handleEditConversation}
102138
onDelete={handleDeleteConversation}
103139
/>
104140
</Sidebar.MenuItem>
@@ -119,7 +155,53 @@
119155
</Sidebar.GroupContent>
120156
</Sidebar.Group>
121157

122-
<div class="bottom-0 z-10 bg-sidebar bg-sidebar/50 px-4 py-4 backdrop-blur-lg md:sticky">
123-
<p class="text-xs text-muted-foreground">Conversations are stored locally in your browser.</p>
124-
</div>
158+
<div class="bottom-0 z-10 bg-sidebar bg-sidebar/50 px-4 py-4 backdrop-blur-lg md:sticky"></div>
125159
</ScrollArea>
160+
161+
<ConfirmationDialog
162+
bind:open={showDeleteDialog}
163+
title="Delete Conversation"
164+
description={selectedConversation
165+
? `Are you sure you want to delete "${selectedConversation.name}"? This action cannot be undone and will permanently remove all messages in this conversation.`
166+
: ''}
167+
confirmText="Delete"
168+
cancelText="Cancel"
169+
variant="destructive"
170+
icon={Trash2}
171+
onConfirm={handleConfirmDelete}
172+
onCancel={() => {
173+
showDeleteDialog = false;
174+
selectedConversation = null;
175+
}}
176+
/>
177+
178+
<AlertDialog.Root bind:open={showEditDialog}>
179+
<AlertDialog.Content>
180+
<AlertDialog.Header>
181+
<AlertDialog.Title>Edit Conversation Name</AlertDialog.Title>
182+
<AlertDialog.Description>
183+
<Input
184+
class="mt-4 text-foreground"
185+
onkeydown={(e) => {
186+
if (e.key === 'Enter') {
187+
e.preventDefault();
188+
handleConfirmEdit();
189+
}
190+
}}
191+
placeholder="Enter a new name"
192+
type="text"
193+
bind:value={editedName}
194+
/>
195+
</AlertDialog.Description>
196+
</AlertDialog.Header>
197+
<AlertDialog.Footer>
198+
<AlertDialog.Cancel
199+
onclick={() => {
200+
showEditDialog = false;
201+
selectedConversation = null;
202+
}}>Cancel</AlertDialog.Cancel
203+
>
204+
<AlertDialog.Action onclick={handleConfirmEdit}>Save</AlertDialog.Action>
205+
</AlertDialog.Footer>
206+
</AlertDialog.Content>
207+
</AlertDialog.Root>

tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebarConversationItem.svelte

Lines changed: 60 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
<script lang="ts">
22
import { Trash2, Pencil, MoreHorizontal } from '@lucide/svelte';
3-
import { ActionDropdown, ConfirmationDialog } from '$lib/components/app';
4-
import * as AlertDialog from '$lib/components/ui/alert-dialog';
5-
import Input from '$lib/components/ui/input/input.svelte';
3+
import { ActionDropdown } from '$lib/components/app';
64
import { onMount } from 'svelte';
75
86
interface Props {
97
isActive?: boolean;
108
conversation: DatabaseConversation;
119
handleMobileSidebarItemClick?: () => void;
1210
onDelete?: (id: string) => void;
13-
onEdit?: (id: string, name: string) => void;
11+
onEdit?: (id: string) => void;
1412
onSelect?: (id: string) => void;
15-
showLastModified?: boolean;
1613
}
1714
1815
let {
@@ -21,54 +18,48 @@
2118
onDelete,
2219
onEdit,
2320
onSelect,
24-
isActive = false,
25-
showLastModified = false
21+
isActive = false
2622
}: Props = $props();
2723
28-
let editedName = $state('');
29-
let showDeleteDialog = $state(false);
30-
let showDropdown = $state(false);
31-
let showEditDialog = $state(false);
32-
33-
function formatLastModified(timestamp: number) {
34-
const now = Date.now();
35-
const diff = now - timestamp;
36-
const minutes = Math.floor(diff / (1000 * 60));
37-
const hours = Math.floor(diff / (1000 * 60 * 60));
38-
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
39-
40-
if (minutes < 1) return 'Just now';
41-
if (minutes < 60) return `${minutes}m ago`;
42-
if (hours < 24) return `${hours}h ago`;
43-
return `${days}d ago`;
24+
let renderActionsDropdown = $state(false);
25+
let dropdownOpen = $state(false);
26+
27+
function handleEdit(event: Event) {
28+
event.stopPropagation();
29+
onEdit?.(conversation.id);
4430
}
4531
46-
function handleConfirmDelete() {
32+
function handleDelete(event: Event) {
33+
event.stopPropagation();
4734
onDelete?.(conversation.id);
4835
}
4936
50-
function handleConfirmEdit() {
51-
if (!editedName.trim()) return;
52-
showEditDialog = false;
53-
onEdit?.(conversation.id, editedName);
37+
function handleGlobalEditEvent(event: Event) {
38+
const customEvent = event as CustomEvent<{ conversationId: string }>;
39+
if (customEvent.detail.conversationId === conversation.id && isActive) {
40+
handleEdit(event);
41+
}
5442
}
5543
56-
function handleEdit(event: Event) {
57-
event.stopPropagation();
58-
editedName = conversation.name;
59-
showEditDialog = true;
44+
function handleMouseLeave() {
45+
if (!dropdownOpen) {
46+
renderActionsDropdown = false;
47+
}
48+
}
49+
50+
function handleMouseOver() {
51+
renderActionsDropdown = true;
6052
}
6153
6254
function handleSelect() {
6355
onSelect?.(conversation.id);
6456
}
6557
66-
function handleGlobalEditEvent(event: Event) {
67-
const customEvent = event as CustomEvent<{ conversationId: string }>;
68-
if (customEvent.detail.conversationId === conversation.id && isActive) {
69-
handleEdit(event);
58+
$effect(() => {
59+
if (!dropdownOpen) {
60+
renderActionsDropdown = false;
7061
}
71-
}
62+
});
7263
7364
onMount(() => {
7465
document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
@@ -82,99 +73,46 @@
8273
});
8374
</script>
8475

76+
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
8577
<button
86-
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
78+
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
8779
? 'bg-foreground/5 text-accent-foreground'
8880
: ''}"
8981
onclick={handleSelect}
82+
onmouseover={handleMouseOver}
83+
onmouseleave={handleMouseLeave}
9084
>
9185
<!-- svelte-ignore a11y_click_events_have_key_events -->
9286
<!-- svelte-ignore a11y_no_static_element_interactions -->
93-
<div
94-
class="text flex min-w-0 flex-1 items-center space-x-3"
95-
onclick={handleMobileSidebarItemClick}
96-
>
97-
<div class="min-w-0 flex-1">
98-
<p class="truncate text-sm font-medium">{conversation.name}</p>
99-
100-
{#if showLastModified}
101-
<div class="mt-2 flex flex-wrap items-center space-y-2 space-x-2">
102-
<span class="w-full text-xs text-muted-foreground">
103-
{formatLastModified(conversation.lastModified)}
104-
</span>
105-
</div>
106-
{/if}
107-
</div>
108-
</div>
109-
110-
<div class="actions flex items-center">
111-
<ActionDropdown
112-
triggerIcon={MoreHorizontal}
113-
triggerTooltip="More actions"
114-
bind:open={showDropdown}
115-
actions={[
116-
{
117-
icon: Pencil,
118-
label: 'Edit',
119-
onclick: handleEdit,
120-
shortcut: ['shift', 'cmd', 'e']
121-
},
122-
{
123-
icon: Trash2,
124-
label: 'Delete',
125-
onclick: (e) => {
126-
e.stopPropagation();
127-
showDeleteDialog = true;
87+
<span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}>
88+
{conversation.name}
89+
</span>
90+
91+
{#if renderActionsDropdown}
92+
<div class="actions flex items-center">
93+
<ActionDropdown
94+
triggerIcon={MoreHorizontal}
95+
triggerTooltip="More actions"
96+
bind:open={dropdownOpen}
97+
actions={[
98+
{
99+
icon: Pencil,
100+
label: 'Edit',
101+
onclick: handleEdit,
102+
shortcut: ['shift', 'cmd', 'e']
128103
},
129-
variant: 'destructive',
130-
shortcut: ['shift', 'cmd', 'd'],
131-
separator: true
132-
}
133-
]}
134-
/>
135-
136-
<ConfirmationDialog
137-
bind:open={showDeleteDialog}
138-
title="Delete Conversation"
139-
description={`Are you sure you want to delete "${conversation.name}"? This action cannot be undone and will permanently remove all messages in this conversation.`}
140-
confirmText="Delete"
141-
cancelText="Cancel"
142-
variant="destructive"
143-
icon={Trash2}
144-
onConfirm={handleConfirmDelete}
145-
onCancel={() => (showDeleteDialog = false)}
146-
/>
147-
148-
<AlertDialog.Root bind:open={showEditDialog}>
149-
<AlertDialog.Content>
150-
<AlertDialog.Header>
151-
<AlertDialog.Title>Edit Conversation Name</AlertDialog.Title>
152-
153-
<AlertDialog.Description>
154-
<Input
155-
class="mt-4 text-foreground"
156-
onkeydown={(e) => {
157-
if (e.key === 'Enter') {
158-
e.preventDefault();
159-
handleConfirmEdit();
160-
showEditDialog = false;
161-
}
162-
}}
163-
placeholder="Enter a new name"
164-
type="text"
165-
bind:value={editedName}
166-
/>
167-
</AlertDialog.Description>
168-
</AlertDialog.Header>
169-
170-
<AlertDialog.Footer>
171-
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
172-
173-
<AlertDialog.Action onclick={handleConfirmEdit}>Save</AlertDialog.Action>
174-
</AlertDialog.Footer>
175-
</AlertDialog.Content>
176-
</AlertDialog.Root>
177-
</div>
104+
{
105+
icon: Trash2,
106+
label: 'Delete',
107+
onclick: handleDelete,
108+
variant: 'destructive',
109+
shortcut: ['shift', 'cmd', 'd'],
110+
separator: true
111+
}
112+
]}
113+
/>
114+
</div>
115+
{/if}
178116
</button>
179117

180118
<style>

tools/server/webui/src/routes/+layout.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@
140140
});
141141
</script>
142142

143+
<svelte:window onkeydown={handleKeydown} />
144+
143145
<ModeWatcher />
144146

145147
<Toaster richColors />
@@ -172,5 +174,3 @@
172174
</Sidebar.Inset>
173175
</div>
174176
</Sidebar.Provider>
175-
176-
<svelte:window onkeydown={handleKeydown} />

0 commit comments

Comments
 (0)