1<script lang="ts">
2 import { Trash2, Pencil, MoreHorizontal, Download, Loader2, Square } from '@lucide/svelte';
3 import { ActionDropdown } from '$lib/components/app';
4 import * as Tooltip from '$lib/components/ui/tooltip';
5 import { getAllLoadingChats } from '$lib/stores/chat.svelte';
6 import { conversationsStore } from '$lib/stores/conversations.svelte';
7 import { onMount } from 'svelte';
8
9 interface Props {
10 isActive?: boolean;
11 conversation: DatabaseConversation;
12 handleMobileSidebarItemClick?: () => void;
13 onDelete?: (id: string) => void;
14 onEdit?: (id: string) => void;
15 onSelect?: (id: string) => void;
16 onStop?: (id: string) => void;
17 }
18
19 let {
20 conversation,
21 handleMobileSidebarItemClick,
22 onDelete,
23 onEdit,
24 onSelect,
25 onStop,
26 isActive = false
27 }: Props = $props();
28
29 let renderActionsDropdown = $state(false);
30 let dropdownOpen = $state(false);
31
32 let isLoading = $derived(getAllLoadingChats().includes(conversation.id));
33
34 function handleEdit(event: Event) {
35 event.stopPropagation();
36 onEdit?.(conversation.id);
37 }
38
39 function handleDelete(event: Event) {
40 event.stopPropagation();
41 onDelete?.(conversation.id);
42 }
43
44 function handleStop(event: Event) {
45 event.stopPropagation();
46 onStop?.(conversation.id);
47 }
48
49 function handleGlobalEditEvent(event: Event) {
50 const customEvent = event as CustomEvent<{ conversationId: string }>;
51
52 if (customEvent.detail.conversationId === conversation.id && isActive) {
53 handleEdit(event);
54 }
55 }
56
57 function handleMouseLeave() {
58 if (!dropdownOpen) {
59 renderActionsDropdown = false;
60 }
61 }
62
63 function handleMouseOver() {
64 renderActionsDropdown = true;
65 }
66
67 function handleSelect() {
68 onSelect?.(conversation.id);
69 }
70
71 $effect(() => {
72 if (!dropdownOpen) {
73 renderActionsDropdown = false;
74 }
75 });
76
77 onMount(() => {
78 document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
79
80 return () => {
81 document.removeEventListener(
82 'edit-active-conversation',
83 handleGlobalEditEvent as EventListener
84 );
85 };
86 });
87</script>
88
89<!-- svelte-ignore a11y_mouse_events_have_key_events -->
90<button
91 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
92 ? 'bg-foreground/5 text-accent-foreground'
93 : ''}"
94 onclick={handleSelect}
95 onmouseover={handleMouseOver}
96 onmouseleave={handleMouseLeave}
97>
98 <div class="flex min-w-0 flex-1 items-center gap-2">
99 {#if isLoading}
100 <Tooltip.Root>
101 <Tooltip.Trigger>
102 <div
103 class="stop-button flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
104 onclick={handleStop}
105 onkeydown={(e) => e.key === 'Enter' && handleStop(e)}
106 role="button"
107 tabindex="0"
108 aria-label="Stop generation"
109 >
110 <Loader2 class="loading-icon h-3.5 w-3.5 animate-spin" />
111
112 <Square class="stop-icon hidden h-3 w-3 fill-current text-destructive" />
113 </div>
114 </Tooltip.Trigger>
115
116 <Tooltip.Content>
117 <p>Stop generation</p>
118 </Tooltip.Content>
119 </Tooltip.Root>
120 {/if}
121
122 <!-- svelte-ignore a11y_click_events_have_key_events -->
123 <!-- svelte-ignore a11y_no_static_element_interactions -->
124 <span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}>
125 {conversation.name}
126 </span>
127 </div>
128
129 {#if renderActionsDropdown}
130 <div class="actions flex items-center">
131 <ActionDropdown
132 triggerIcon={MoreHorizontal}
133 triggerTooltip="More actions"
134 bind:open={dropdownOpen}
135 actions={[
136 {
137 icon: Pencil,
138 label: 'Edit',
139 onclick: handleEdit,
140 shortcut: ['shift', 'cmd', 'e']
141 },
142 {
143 icon: Download,
144 label: 'Export',
145 onclick: (e) => {
146 e.stopPropagation();
147 conversationsStore.downloadConversation(conversation.id);
148 },
149 shortcut: ['shift', 'cmd', 's']
150 },
151 {
152 icon: Trash2,
153 label: 'Delete',
154 onclick: handleDelete,
155 variant: 'destructive',
156 shortcut: ['shift', 'cmd', 'd'],
157 separator: true
158 }
159 ]}
160 />
161 </div>
162 {/if}
163</button>
164
165<style>
166 button {
167 :global([data-slot='dropdown-menu-trigger']:not([data-state='open'])) {
168 opacity: 0;
169 }
170
171 &:is(:hover) :global([data-slot='dropdown-menu-trigger']) {
172 opacity: 1;
173 }
174 @media (max-width: 768px) {
175 :global([data-slot='dropdown-menu-trigger']) {
176 opacity: 1 !important;
177 }
178 }
179
180 .stop-button {
181 :global(.stop-icon) {
182 display: none;
183 }
184
185 :global(.loading-icon) {
186 display: block;
187 }
188 }
189
190 &:is(:hover) .stop-button {
191 :global(.stop-icon) {
192 display: block;
193 }
194
195 :global(.loading-icon) {
196 display: none;
197 }
198 }
199 }
200</style>