]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
7204d7fec293566e34b08912cc452de84f81b29d
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
3 import {
4 Trash2,
5 Pencil,
6 MoreHorizontal,
7 Download,
8 Loader2,
9 Square,
10 GitBranch,
11 Pin,
12 PinOff,
13 ListChecks
14 } from '@lucide/svelte';
15 import { DropdownMenuActions } from '$lib/components/app';
16 import * as Tooltip from '$lib/components/ui/tooltip';
17 import { Checkbox } from '$lib/components/ui/checkbox';
18 import { FORK_TREE_DEPTH_PADDING } from '$lib/constants';
19 import { RouterService } from '$lib/services/router.service';
20 import { getAllLoadingChats } from '$lib/stores/chat.svelte';
21 import { conversationsStore } from '$lib/stores/conversations.svelte';
22 import { TruncatedText } from '$lib/components/app';
23 import { onMount } from 'svelte';
24
25 interface Props {
26 isActive?: boolean;
27 depth?: number;
28 conversation: DatabaseConversation;
29 isSelectionMode?: boolean;
30 isSelected?: boolean;
31 onDelete?: (id: string) => void;
32 onEdit?: (id: string) => void;
33 onSelect?: (id: string) => void;
34 onStop?: (id: string) => void;
35 onToggleSelect?: (id: string) => void;
36 onEnterSelectionMode?: (id: string) => void;
37 onSelectionClick?: (id: string, options: { shiftKey: boolean }) => void;
38 onRowMouseDown?: (id: string, event: MouseEvent) => void;
39 }
40
41 let {
42 conversation,
43 onDelete,
44 onEdit,
45 onSelect,
46 onStop,
47 onToggleSelect,
48 onEnterSelectionMode,
49 onSelectionClick,
50 onRowMouseDown,
51 isActive = false,
52 isSelectionMode = false,
53 isSelected = false,
54 depth = 0
55 }: Props = $props();
56
57 let renderActionsDropdown = $state(false);
58 let dropdownOpen = $state(false);
59
60 let isLoading = $derived(getAllLoadingChats().includes(conversation.id));
61
62 function handleEdit(event: Event) {
63 event.stopPropagation();
64 onEdit?.(conversation.id);
65 }
66
67 function handleDelete(event: Event) {
68 event.stopPropagation();
69 onDelete?.(conversation.id);
70 }
71
72 function handleStop(event: Event) {
73 event.stopPropagation();
74 onStop?.(conversation.id);
75 }
76
77 function handleTogglePin() {
78 conversationsStore.toggleConversationPin(conversation.id);
79 }
80
81 function handleEnterSelectionMode(event: Event) {
82 event.stopPropagation();
83 onEnterSelectionMode?.(conversation.id);
84 }
85
86 function handleGlobalEditEvent(event: Event) {
87 const customEvent = event as CustomEvent<{ conversationId: string }>;
88
89 if (customEvent.detail.conversationId === conversation.id && isActive) {
90 handleEdit(event);
91 }
92 }
93
94 function handleMouseLeave() {
95 if (!dropdownOpen) {
96 renderActionsDropdown = false;
97 }
98 }
99
100 function handleMouseOver() {
101 if (isSelectionMode) return;
102 renderActionsDropdown = true;
103 }
104
105 function handleSelect(event: MouseEvent) {
106 if (isSelectionMode) {
107 onSelectionClick?.(conversation.id, { shiftKey: event.shiftKey });
108 } else {
109 onSelect?.(conversation.id);
110 }
111 }
112
113 function handleCheckboxClick(event: MouseEvent) {
114 event.stopPropagation();
115 if (isSelectionMode) {
116 onSelectionClick?.(conversation.id, { shiftKey: event.shiftKey });
117 } else {
118 onToggleSelect?.(conversation.id);
119 }
120 }
121
122 function handleRowMouseDown(event: MouseEvent) {
123 onRowMouseDown?.(conversation.id, event);
124 }
125
126 function handleCheckboxKeydown(event: KeyboardEvent) {
127 if (event.key !== ' ' && event.key !== 'Enter') return;
128 event.stopPropagation();
129 event.preventDefault();
130 if (isSelectionMode) {
131 onSelectionClick?.(conversation.id, { shiftKey: event.shiftKey });
132 } else {
133 onToggleSelect?.(conversation.id);
134 }
135 }
136
137 $effect(() => {
138 if (!dropdownOpen) {
139 renderActionsDropdown = false;
140 }
141 });
142
143 onMount(() => {
144 document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
145
146 return () => {
147 document.removeEventListener(
148 'edit-active-conversation',
149 handleGlobalEditEvent as EventListener
150 );
151 };
152 });
153 </script>
154
155 <!-- svelte-ignore a11y_mouse_events_have_key_events -->
156 <button
157 class="group flex min-h-9 w-full cursor-pointer items-center justify-between space-x-3 rounded-lg py-1.5 text-left transition-colors hover:bg-foreground/10 {isActive
158 ? 'bg-foreground/5 text-accent-foreground'
159 : ''} {isSelected ? 'bg-primary/10 hover:bg-primary/15' : ''} {isSelectionMode
160 ? 'is-selection-mode'
161 : ''} px-2"
162 data-conversation-row={conversation.id}
163 onclick={(e) => handleSelect(e)}
164 onmouseover={handleMouseOver}
165 onmouseleave={handleMouseLeave}
166 onmousedown={(e) => handleRowMouseDown(e)}
167 onfocusin={handleMouseOver}
168 onfocusout={(e) => {
169 if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
170 handleMouseLeave();
171 }
172 }}
173 >
174 <div
175 class="flex min-w-0 flex-1 items-center gap-2"
176 style:padding-left="{depth * FORK_TREE_DEPTH_PADDING}px"
177 >
178 {#if isSelectionMode}
179 <div
180 class="shrink-0"
181 onclick={(e) => handleCheckboxClick(e)}
182 onkeydown={handleCheckboxKeydown}
183 role="checkbox"
184 aria-checked={isSelected}
185 aria-label={isSelected ? `Deselect ${conversation.name}` : `Select ${conversation.name}`}
186 tabindex="-1"
187 >
188 <Checkbox
189 checked={isSelected}
190 aria-label={isSelected ? `Deselect ${conversation.name}` : `Select ${conversation.name}`}
191 />
192 </div>
193 {/if}
194
195 {#if depth > 0}
196 <Tooltip.Root>
197 <Tooltip.Trigger>
198 <!-- prevent another nested button element -->
199 {#snippet child({ props })}
200 <a
201 {...props}
202 href={RouterService.chat(conversation.forkedFromConversationId)}
203 class="flex shrink-0 items-center text-muted-foreground transition-colors hover:text-foreground"
204 >
205 <GitBranch class="h-3.5 w-3.5" />
206 </a>
207 {/snippet}
208 </Tooltip.Trigger>
209
210 <Tooltip.Content>
211 <p>See parent conversation</p>
212 </Tooltip.Content>
213 </Tooltip.Root>
214 {/if}
215
216 {#if isLoading}
217 <Tooltip.Root>
218 <Tooltip.Trigger>
219 <div
220 class="stop-button flex {ICON_CLASS_DEFAULT} shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
221 onclick={handleStop}
222 onkeydown={(e) => e.key === 'Enter' && handleStop(e)}
223 role="button"
224 tabindex="0"
225 aria-label="Stop generation"
226 >
227 <Loader2 class="loading-icon h-3.5 w-3.5 animate-spin" />
228
229 <Square class="stop-icon hidden h-3 w-3 fill-current text-destructive" />
230 </div>
231 </Tooltip.Trigger>
232
233 <Tooltip.Content>
234 <p>Stop generation</p>
235 </Tooltip.Content>
236 </Tooltip.Root>
237 {/if}
238
239 <TruncatedText text={conversation.name} class="text-sm font-medium" showTooltip={false} />
240 </div>
241
242 {#if !isSelectionMode && renderActionsDropdown}
243 <div class="actions flex items-center">
244 <DropdownMenuActions
245 triggerIcon={MoreHorizontal}
246 triggerTooltip="More actions"
247 bind:open={dropdownOpen}
248 actions={[
249 {
250 icon: conversation.pinned ? PinOff : Pin,
251 label: conversation.pinned ? 'Unpin' : 'Pin',
252 onclick: (e: Event) => {
253 e.stopPropagation();
254 handleTogglePin();
255 }
256 },
257 {
258 icon: Pencil,
259 label: 'Edit',
260 onclick: handleEdit,
261 shortcut: ['shift', 'cmd', 'e']
262 },
263 {
264 icon: Download,
265 label: 'Export',
266 onclick: (e: Event) => {
267 e.stopPropagation();
268 conversationsStore.downloadConversation(conversation.id);
269 },
270 shortcut: ['shift', 'cmd', 's']
271 },
272 {
273 icon: ListChecks,
274 label: 'Select',
275 onclick: handleEnterSelectionMode
276 },
277 {
278 icon: Trash2,
279 label: 'Delete',
280 onclick: handleDelete,
281 variant: 'destructive',
282 shortcut: ['shift', 'cmd', 'd'],
283 separator: true
284 }
285 ]}
286 />
287 </div>
288 {/if}
289 </button>
290
291 <style>
292 button {
293 :global([data-slot='dropdown-menu-trigger']:not([data-state='open'])) {
294 opacity: 0;
295 }
296
297 &:is(:hover) :global([data-slot='dropdown-menu-trigger']),
298 &:focus-within :global([data-slot='dropdown-menu-trigger']) {
299 opacity: 1;
300 }
301 @media (max-width: 768px) {
302 :global([data-slot='dropdown-menu-trigger']) {
303 opacity: 1 !important;
304 }
305 }
306
307 &.is-selection-mode :global([data-slot='dropdown-menu-trigger']) {
308 display: none !important;
309 }
310
311 .stop-button {
312 :global(.stop-icon) {
313 display: none;
314 }
315
316 :global(.loading-icon) {
317 display: block;
318 }
319 }
320
321 &:is(:hover) .stop-button {
322 :global(.stop-icon) {
323 display: block;
324 }
325
326 :global(.loading-icon) {
327 display: none;
328 }
329 }
330 }
331 </style>