]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
08c66a8a9768cc3745e74525b11943f8ac49d180
[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 } from '@lucide/svelte';
14 import { DropdownMenuActions } from '$lib/components/app';
15 import * as Tooltip from '$lib/components/ui/tooltip';
16 import { FORK_TREE_DEPTH_PADDING } from '$lib/constants';
17 import { RouterService } from '$lib/services/router.service';
18 import { getAllLoadingChats } from '$lib/stores/chat.svelte';
19 import { conversationsStore } from '$lib/stores/conversations.svelte';
20 import { TruncatedText } from '$lib/components/app';
21 import { onMount } from 'svelte';
22
23 interface Props {
24 isActive?: boolean;
25 depth?: number;
26 conversation: DatabaseConversation;
27 onDelete?: (id: string) => void;
28 onEdit?: (id: string) => void;
29 onSelect?: (id: string) => void;
30 onStop?: (id: string) => void;
31 }
32
33 let {
34 conversation,
35 onDelete,
36 onEdit,
37 onSelect,
38 onStop,
39 isActive = false,
40 depth = 0
41 }: Props = $props();
42
43 let renderActionsDropdown = $state(false);
44 let dropdownOpen = $state(false);
45
46 let isLoading = $derived(getAllLoadingChats().includes(conversation.id));
47
48 function handleEdit(event: Event) {
49 event.stopPropagation();
50 onEdit?.(conversation.id);
51 }
52
53 function handleDelete(event: Event) {
54 event.stopPropagation();
55 onDelete?.(conversation.id);
56 }
57
58 function handleStop(event: Event) {
59 event.stopPropagation();
60 onStop?.(conversation.id);
61 }
62
63 function handleTogglePin() {
64 conversationsStore.toggleConversationPin(conversation.id);
65 }
66
67 function handleGlobalEditEvent(event: Event) {
68 const customEvent = event as CustomEvent<{ conversationId: string }>;
69
70 if (customEvent.detail.conversationId === conversation.id && isActive) {
71 handleEdit(event);
72 }
73 }
74
75 function handleMouseLeave() {
76 if (!dropdownOpen) {
77 renderActionsDropdown = false;
78 }
79 }
80
81 function handleMouseOver() {
82 renderActionsDropdown = true;
83 }
84
85 function handleSelect() {
86 onSelect?.(conversation.id);
87 }
88
89 $effect(() => {
90 if (!dropdownOpen) {
91 renderActionsDropdown = false;
92 }
93 });
94
95 onMount(() => {
96 document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
97
98 return () => {
99 document.removeEventListener(
100 'edit-active-conversation',
101 handleGlobalEditEvent as EventListener
102 );
103 };
104 });
105 </script>
106
107 <!-- svelte-ignore a11y_mouse_events_have_key_events -->
108 <button
109 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
110 ? 'bg-foreground/5 text-accent-foreground'
111 : ''} px-3"
112 onclick={handleSelect}
113 onmouseover={handleMouseOver}
114 onmouseleave={handleMouseLeave}
115 onfocusin={handleMouseOver}
116 onfocusout={(e) => {
117 if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
118 handleMouseLeave();
119 }
120 }}
121 >
122 <div
123 class="flex min-w-0 flex-1 items-center gap-2"
124 style:padding-left="{depth * FORK_TREE_DEPTH_PADDING}px"
125 >
126 {#if depth > 0}
127 <Tooltip.Root>
128 <Tooltip.Trigger>
129 <!-- prevent another nested button element -->
130 {#snippet child({ props })}
131 <a
132 {...props}
133 href={RouterService.chat(conversation.forkedFromConversationId)}
134 class="flex shrink-0 items-center text-muted-foreground transition-colors hover:text-foreground"
135 >
136 <GitBranch class="h-3.5 w-3.5" />
137 </a>
138 {/snippet}
139 </Tooltip.Trigger>
140
141 <Tooltip.Content>
142 <p>See parent conversation</p>
143 </Tooltip.Content>
144 </Tooltip.Root>
145 {/if}
146
147 {#if isLoading}
148 <Tooltip.Root>
149 <Tooltip.Trigger>
150 <div
151 class="stop-button flex {ICON_CLASS_DEFAULT} shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
152 onclick={handleStop}
153 onkeydown={(e) => e.key === 'Enter' && handleStop(e)}
154 role="button"
155 tabindex="0"
156 aria-label="Stop generation"
157 >
158 <Loader2 class="loading-icon h-3.5 w-3.5 animate-spin" />
159
160 <Square class="stop-icon hidden h-3 w-3 fill-current text-destructive" />
161 </div>
162 </Tooltip.Trigger>
163
164 <Tooltip.Content>
165 <p>Stop generation</p>
166 </Tooltip.Content>
167 </Tooltip.Root>
168 {/if}
169
170 <TruncatedText text={conversation.name} class="text-sm font-medium" showTooltip={false} />
171 </div>
172
173 {#if renderActionsDropdown}
174 <div class="actions flex items-center">
175 <DropdownMenuActions
176 triggerIcon={MoreHorizontal}
177 triggerTooltip="More actions"
178 bind:open={dropdownOpen}
179 actions={[
180 {
181 icon: conversation.pinned ? PinOff : Pin,
182 label: conversation.pinned ? 'Unpin' : 'Pin',
183 onclick: (e: Event) => {
184 e.stopPropagation();
185 handleTogglePin();
186 }
187 },
188 {
189 icon: Pencil,
190 label: 'Edit',
191 onclick: handleEdit,
192 shortcut: ['shift', 'cmd', 'e']
193 },
194 {
195 icon: Download,
196 label: 'Export',
197 onclick: (e: Event) => {
198 e.stopPropagation();
199 conversationsStore.downloadConversation(conversation.id);
200 },
201 shortcut: ['shift', 'cmd', 's']
202 },
203 {
204 icon: Trash2,
205 label: 'Delete',
206 onclick: handleDelete,
207 variant: 'destructive',
208 shortcut: ['shift', 'cmd', 'd'],
209 separator: true
210 }
211 ]}
212 />
213 </div>
214 {/if}
215 </button>
216
217 <style>
218 button {
219 :global([data-slot='dropdown-menu-trigger']:not([data-state='open'])) {
220 opacity: 0;
221 }
222
223 &:is(:hover) :global([data-slot='dropdown-menu-trigger']),
224 &:focus-within :global([data-slot='dropdown-menu-trigger']) {
225 opacity: 1;
226 }
227 @media (max-width: 768px) {
228 :global([data-slot='dropdown-menu-trigger']) {
229 opacity: 1 !important;
230 }
231 }
232
233 .stop-button {
234 :global(.stop-icon) {
235 display: none;
236 }
237
238 :global(.loading-icon) {
239 display: block;
240 }
241 }
242
243 &:is(:hover) .stop-button {
244 :global(.stop-icon) {
245 display: block;
246 }
247
248 :global(.loading-icon) {
249 display: none;
250 }
251 }
252 }
253 </style>