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