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