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