]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
2c1b9adf21ba908f03ab612c633c0ff24ccc8570
[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 dropdownOpen = $state(false);
43
44 let isLoading = $derived(getAllLoadingChats().includes(conversation.id));
45
46 function handleEdit(event: Event) {
47 event.stopPropagation();
48 onEdit?.(conversation.id);
49 }
50
51 function handleDelete(event: Event) {
52 event.stopPropagation();
53 onDelete?.(conversation.id);
54 }
55
56 function handleStop(event: Event) {
57 event.stopPropagation();
58 onStop?.(conversation.id);
59 }
60
61 function handleTogglePin() {
62 conversationsStore.toggleConversationPin(conversation.id);
63 }
64
65 function handleGlobalEditEvent(event: Event) {
66 const customEvent = event as CustomEvent<{ conversationId: string }>;
67
68 if (customEvent.detail.conversationId === conversation.id && isActive) {
69 handleEdit(event);
70 }
71 }
72
73 function handleSelect() {
74 onSelect?.(conversation.id);
75 }
76
77 onMount(() => {
78 document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
79
80 return () => {
81 document.removeEventListener(
82 'edit-active-conversation',
83 handleGlobalEditEvent as EventListener
84 );
85 };
86 });
87 </script>
88
89 <div
90 class="conversation-item group relative flex min-h-9 w-full items-center justify-between space-x-3 rounded-lg py-1.5 transition-colors hover:bg-foreground/10 {isActive
91 ? 'bg-foreground/5 text-accent-foreground'
92 : ''} px-3"
93 >
94 <button
95 class="absolute inset-0 z-0 cursor-pointer rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
96 onclick={handleSelect}
97 aria-label={conversation.name}
98 >
99 </button>
100 <div
101 class="pointer-events-none relative z-10 flex min-w-0 flex-1 items-center gap-2"
102 style:padding-left="{depth * FORK_TREE_DEPTH_PADDING}px"
103 >
104 {#if depth > 0}
105 <Tooltip.Root>
106 <Tooltip.Trigger>
107 <!-- prevent another nested button element -->
108 {#snippet child({ props })}
109 <a
110 {...props}
111 href={RouterService.chat(conversation.forkedFromConversationId)}
112 class="pointer-events-auto flex shrink-0 items-center text-muted-foreground transition-colors hover:text-foreground"
113 >
114 <GitBranch class="h-3.5 w-3.5" />
115 </a>
116 {/snippet}
117 </Tooltip.Trigger>
118
119 <Tooltip.Content>
120 <p>See parent conversation</p>
121 </Tooltip.Content>
122 </Tooltip.Root>
123 {/if}
124
125 {#if isLoading}
126 <Tooltip.Root>
127 <Tooltip.Trigger>
128 <button
129 class="stop-button pointer-events-auto flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
130 onclick={handleStop}
131 aria-label="Stop generation"
132 >
133 <Loader2 class="loading-icon h-3.5 w-3.5 animate-spin" />
134
135 <Square class="stop-icon hidden h-3 w-3 fill-current text-destructive" />
136 </button>
137 </Tooltip.Trigger>
138
139 <Tooltip.Content>
140 <p>Stop generation</p>
141 </Tooltip.Content>
142 </Tooltip.Root>
143 {/if}
144
145 <TruncatedText text={conversation.name} class="text-sm font-medium" showTooltip={false} />
146 </div>
147
148 <div class="actions pointer-events-auto relative z-20 flex items-center">
149 <DropdownMenuActions
150 triggerIcon={MoreHorizontal}
151 triggerTooltip="More actions"
152 bind:open={dropdownOpen}
153 actions={[
154 {
155 icon: conversation.pinned ? PinOff : Pin,
156 label: conversation.pinned ? 'Unpin' : 'Pin',
157 onclick: (e: Event) => {
158 e.stopPropagation();
159 handleTogglePin();
160 }
161 },
162 {
163 icon: Pencil,
164 label: 'Edit',
165 onclick: handleEdit,
166 shortcut: ['shift', 'cmd', 'e']
167 },
168 {
169 icon: Download,
170 label: 'Export',
171 onclick: (e: Event) => {
172 e.stopPropagation();
173 conversationsStore.downloadConversation(conversation.id);
174 },
175 shortcut: ['shift', 'cmd', 's']
176 },
177 {
178 icon: Trash2,
179 label: 'Delete',
180 onclick: handleDelete,
181 variant: 'destructive',
182 shortcut: ['shift', 'cmd', 'd'],
183 separator: true
184 }
185 ]}
186 />
187 </div>
188 </div>
189
190 <style>
191 .conversation-item {
192 :global([data-slot='dropdown-menu-trigger']:not([data-state='open'])) {
193 opacity: 0;
194 }
195
196 &:is(:hover) :global([data-slot='dropdown-menu-trigger']),
197 &:focus-within :global([data-slot='dropdown-menu-trigger']) {
198 opacity: 1;
199 }
200 @media (max-width: 768px) {
201 :global([data-slot='dropdown-menu-trigger']) {
202 opacity: 1 !important;
203 }
204 }
205
206 .stop-button {
207 :global(.stop-icon) {
208 display: none;
209 }
210
211 :global(.loading-icon) {
212 display: block;
213 }
214 }
215
216 &:is(:hover) .stop-button,
217 &:focus-within .stop-button {
218 :global(.stop-icon) {
219 display: block;
220 }
221
222 :global(.loading-icon) {
223 display: none;
224 }
225 }
226 }
227 </style>