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