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