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