Sidebar.svelte 24.9 KB
Newer Older
1
<script lang="ts">
Timothy J. Baek's avatar
Timothy J. Baek committed
2
3
	import { v4 as uuidv4 } from 'uuid';

Timothy J. Baek's avatar
Timothy J. Baek committed
4
5
6
	import fileSaver from 'file-saver';
	const { saveAs } = fileSaver;

7
8
	import { goto, invalidateAll } from '$app/navigation';
	import { page } from '$app/stores';
Timothy J. Baek's avatar
Timothy J. Baek committed
9
	import { user, chats, settings, showSettings, chatId, tags } from '$lib/stores';
10
	import { onMount, getContext } from 'svelte';
11
12
13

	const i18n = getContext('i18n');

Timothy J. Baek's avatar
Timothy J. Baek committed
14
15
16
	import {
		deleteChatById,
		getChatList,
Henry Holloway's avatar
Henry Holloway committed
17
		getChatById,
Timothy J. Baek's avatar
Timothy J. Baek committed
18
		getChatListByTagName,
Timothy J. Baek's avatar
Timothy J. Baek committed
19
20
		updateChatById,
		getAllChatTags
Timothy J. Baek's avatar
Timothy J. Baek committed
21
	} from '$lib/apis/chats';
Jannik Streidl's avatar
Jannik Streidl committed
22
	import { toast } from 'svelte-sonner';
Timothy J. Baek's avatar
Timothy J. Baek committed
23
	import { fade, slide } from 'svelte/transition';
24
	import { WEBUI_BASE_URL } from '$lib/constants';
25
	import Tooltip from '../common/Tooltip.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
26
	import ChatMenu from './Sidebar/ChatMenu.svelte';
27
28
29
30

	let show = false;
	let navElement;

Timothy J. Baek's avatar
Timothy J. Baek committed
31
	let title: string = 'UI';
32
	let search = '';
33

Timothy J. Baek's avatar
Timothy J. Baek committed
34
35
	let selectedChatId = null;

36
37
	let chatDeleteId = null;
	let chatTitleEditId = null;
38
39
40
	let chatTitle = '';

	let showDropdown = false;
41
	let isEditing = false;
42
43

	onMount(async () => {
44
		if (window.innerWidth > 1024) {
Timothy J. Baek's avatar
Timothy J. Baek committed
45
46
47
			show = true;
		}
		await chats.set(await getChatList(localStorage.token));
Timothy J. Baek's avatar
Timothy J. Baek committed
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

		let touchstartX = 0;
		let touchendX = 0;

		function checkDirection() {
			const screenWidth = window.innerWidth;
			const swipeDistance = Math.abs(touchendX - touchstartX);
			if (swipeDistance >= screenWidth / 4) {
				if (touchendX < touchstartX) {
					show = false;
				}
				if (touchendX > touchstartX) {
					show = true;
				}
			}
		}

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
65
		const onTouchStart = (e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
66
			touchstartX = e.changedTouches[0].screenX;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
67
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
68

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
69
		const onTouchEnd = (e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
70
71
			touchendX = e.changedTouches[0].screenX;
			checkDirection();
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
72
73
74
75
76
77
78
79
80
		};

		document.addEventListener('touchstart', onTouchStart);
		document.addEventListener('touchend', onTouchEnd);

		return () => {
			document.removeEventListener('touchstart', onTouchStart);
			document.removeEventListener('touchend', onTouchEnd);
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
	});

	// Helper function to fetch and add chat content to each chat
	const enrichChatsWithContent = async (chatList) => {
		const enrichedChats = await Promise.all(
			chatList.map(async (chat) => {
				const chatDetails = await getChatById(localStorage.token, chat.id).catch((error) => null); // Handle error or non-existent chat gracefully
				if (chatDetails) {
					chat.chat = chatDetails.chat; // Assuming chatDetails.chat contains the chat content
				}
				return chat;
			})
		);

		await chats.set(enrichedChats);
	};
97
98
99
100
101
102

	const loadChat = async (id) => {
		goto(`/c/${id}`);
	};

	const editChatTitle = async (id, _title) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
103
104
105
106
107
108
109
110
111
112
		if (_title === '') {
			toast.error('Title cannot be an empty string.');
		} else {
			title = _title;

			await updateChatById(localStorage.token, id, {
				title: _title
			});
			await chats.set(await getChatList(localStorage.token));
		}
113
114
115
	};

	const deleteChat = async (id) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
116
117
118
		const res = await deleteChatById(localStorage.token, id).catch((error) => {
			toast.error(error);
			chatDeleteId = null;
119

Timothy J. Baek's avatar
Timothy J. Baek committed
120
121
122
123
			return null;
		});

		if (res) {
Timothy J. Baek's avatar
Timothy J. Baek committed
124
125
126
127
			if ($chatId === id) {
				goto('/');
			}

Timothy J. Baek's avatar
Timothy J. Baek committed
128
129
			await chats.set(await getChatList(localStorage.token));
		}
130
	};
131
132
133
134
135
136

	const saveSettings = async (updated) => {
		await settings.set({ ...$settings, ...updated });
		localStorage.setItem('settings', JSON.stringify($settings));
		location.href = '/';
	};
137
138
139
140
</script>

<div
	bind:this={navElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
141
	class="h-screen max-h-[100dvh] min-h-screen {show
Timothy J. Baek's avatar
Timothy J. Baek committed
142
		? 'lg:relative w-[260px]'
Timothy J. Baek's avatar
Timothy J. Baek committed
143
		: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0
144
145
        "
>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
146
	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
147
		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] {show
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
148
149
150
			? ''
			: 'invisible'}"
	>
Timothy J. Baek's avatar
Timothy J. Baek committed
151
		<div class="px-2 flex justify-center space-x-2">
152
			<a
Timothy J. Baek's avatar
Timothy J. Baek committed
153
				id="sidebar-new-chat-button"
154
				class="flex-grow flex justify-between rounded-xl px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
155
				href="/"
Timothy J. Baek's avatar
Timothy J. Baek committed
156
				on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
157
158
					selectedChatId = null;

159
					await goto('/');
160
					const newChatButton = document.getElementById('new-chat-button');
161
162
163
					setTimeout(() => {
						newChatButton?.click();
					}, 0);
164
165
166
				}}
			>
				<div class="flex self-center">
Timothy J. Baek's avatar
Timothy J. Baek committed
167
					<div class="self-center mr-1.5">
168
169
						<img
							src="{WEBUI_BASE_URL}/static/favicon.png"
170
							class=" size-6 -translate-x-1.5 rounded-full"
171
172
							alt="logo"
						/>
173
174
					</div>

175
					<div class=" self-center font-medium text-sm">{$i18n.t('New Chat')}</div>
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
				</div>

				<div class="self-center">
					<svg
						xmlns="http://www.w3.org/2000/svg"
						viewBox="0 0 20 20"
						fill="currentColor"
						class="w-4 h-4"
					>
						<path
							d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
						/>
						<path
							d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"
						/>
					</svg>
				</div>
193
			</a>
194
195
		</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
196
		{#if $user?.role === 'admin'}
Timothy J. Baek's avatar
Timothy J. Baek committed
197
			<div class="px-2 flex justify-center mt-0.5">
198
				<a
199
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
200
					href="/modelfiles"
Timothy J. Baek's avatar
Timothy J. Baek committed
201
202
203
204
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
205
206
207
208
209
210
				>
					<div class="self-center">
						<svg
							xmlns="http://www.w3.org/2000/svg"
							fill="none"
							viewBox="0 0 24 24"
Timothy J. Baek's avatar
Timothy J. Baek committed
211
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
212
213
214
215
216
217
							stroke="currentColor"
							class="w-4 h-4"
						>
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
Timothy J. Baek's avatar
Timothy J. Baek committed
218
								d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 0 0 2.25-2.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v2.25A2.25 2.25 0 0 0 6 10.5Zm0 9.75h2.25A2.25 2.25 0 0 0 10.5 18v-2.25a2.25 2.25 0 0 0-2.25-2.25H6a2.25 2.25 0 0 0-2.25 2.25V18A2.25 2.25 0 0 0 6 20.25Zm9.75-9.75H18a2.25 2.25 0 0 0 2.25-2.25V6A2.25 2.25 0 0 0 18 3.75h-2.25A2.25 2.25 0 0 0 13.5 6v2.25a2.25 2.25 0 0 0 2.25 2.25Z"
Timothy J. Baek's avatar
Timothy J. Baek committed
219
220
221
							/>
						</svg>
					</div>
222

Timothy J. Baek's avatar
Timothy J. Baek committed
223
					<div class="flex self-center">
224
						<div class=" self-center font-medium text-sm">{$i18n.t('Modelfiles')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
225
					</div>
226
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
227
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
228

Timothy J. Baek's avatar
Timothy J. Baek committed
229
			<div class="px-2 flex justify-center">
230
				<a
231
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
232
					href="/prompts"
Timothy J. Baek's avatar
Timothy J. Baek committed
233
234
235
236
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
237
238
239
240
				>
					<div class="self-center">
						<svg
							xmlns="http://www.w3.org/2000/svg"
Timothy J. Baek's avatar
Timothy J. Baek committed
241
242
							fill="none"
							viewBox="0 0 24 24"
Timothy J. Baek's avatar
Timothy J. Baek committed
243
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
244
							stroke="currentColor"
Timothy J. Baek's avatar
Timothy J. Baek committed
245
246
247
							class="w-4 h-4"
						>
							<path
Timothy J. Baek's avatar
Timothy J. Baek committed
248
249
250
								stroke-linecap="round"
								stroke-linejoin="round"
								d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
Timothy J. Baek's avatar
Timothy J. Baek committed
251
252
253
254
255
							/>
						</svg>
					</div>

					<div class="flex self-center">
256
						<div class=" self-center font-medium text-sm">{$i18n.t('Prompts')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
257
					</div>
258
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
259
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
260

Timothy J. Baek's avatar
Timothy J. Baek committed
261
			<div class="px-2 flex justify-center mb-1">
262
				<a
263
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
264
					href="/documents"
Timothy J. Baek's avatar
Timothy J. Baek committed
265
266
267
268
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
269
270
271
272
273
274
				>
					<div class="self-center">
						<svg
							xmlns="http://www.w3.org/2000/svg"
							fill="none"
							viewBox="0 0 24 24"
Timothy J. Baek's avatar
Timothy J. Baek committed
275
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
276
277
278
279
280
281
282
283
284
285
286
287
							stroke="currentColor"
							class="w-4 h-4"
						>
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
								d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
							/>
						</svg>
					</div>

					<div class="flex self-center">
288
						<div class=" self-center font-medium text-sm">{$i18n.t('Documents')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
289
					</div>
290
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
291
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
292
		{/if}
293

294
295
		<div class="relative flex flex-col flex-1 overflow-y-auto">
			{#if !($settings.saveChatHistory ?? true)}
Timothy J. Baek's avatar
Timothy J. Baek committed
296
				<div class="absolute z-40 w-full h-full bg-gray-50/90 dark:bg-black/90 flex justify-center">
297
					<div class=" text-left px-5 py-2">
298
						<div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
299
						<div class="text-xs mt-2">
Jannik Streidl's avatar
Jannik Streidl committed
300
301
302
303
304
							{$i18n.t(
								"When history is turned off, new chats on this browser won't appear in your history on any of your devices."
							)}
							<span class=" font-semibold"
								>{$i18n.t('This setting does not sync across browsers or devices.')}</span
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
							>
						</div>

						<div class="mt-3">
							<button
								class="flex justify-center items-center space-x-1.5 px-3 py-2.5 rounded-lg text-xs bg-gray-200 hover:bg-gray-300 transition text-gray-800 font-medium w-full"
								type="button"
								on:click={() => {
									saveSettings({
										saveChatHistory: true
									});
								}}
							>
								<svg
									xmlns="http://www.w3.org/2000/svg"
									viewBox="0 0 16 16"
									fill="currentColor"
									class="w-3 h-3"
								>
									<path
										fill-rule="evenodd"
										d="M8 1a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0v-6.5A.75.75 0 0 1 8 1ZM4.11 3.05a.75.75 0 0 1 0 1.06 5.5 5.5 0 1 0 7.78 0 .75.75 0 0 1 1.06-1.06 7 7 0 1 1-9.9 0 .75.75 0 0 1 1.06 0Z"
										clip-rule="evenodd"
									/>
								</svg>

Jannik Streidl's avatar
Jannik Streidl committed
331
								<div>{$i18n.t('Enable Chat History')}</div>
332
333
334
							</button>
						</div>
					</div>
335
				</div>
336
			{/if}
337

Timothy J. Baek's avatar
Timothy J. Baek committed
338
			<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
339
				<div class="flex w-full" id="chat-search">
340
					<div class="self-center pl-3 py-2 rounded-l-xl bg-white dark:bg-gray-950">
341
342
343
344
345
346
347
348
349
350
351
352
353
						<svg
							xmlns="http://www.w3.org/2000/svg"
							viewBox="0 0 20 20"
							fill="currentColor"
							class="w-4 h-4"
						>
							<path
								fill-rule="evenodd"
								d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
								clip-rule="evenodd"
							/>
						</svg>
					</div>
354

355
					<input
356
						class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm dark:text-gray-300 dark:bg-gray-950 outline-none"
Jannik Streidl's avatar
Jannik Streidl committed
357
						placeholder={$i18n.t('Search')}
358
						bind:value={search}
Timothy J. Baek's avatar
Timothy J. Baek committed
359
360
361
						on:focus={() => {
							enrichChatsWithContent($chats);
						}}
362
363
364
					/>
				</div>
			</div>
365

Timothy J. Baek's avatar
Timothy J. Baek committed
366
367
368
			{#if $tags.length > 0}
				<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
					<button
369
						class="px-2.5 text-xs font-medium bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
Timothy J. Baek's avatar
Timothy J. Baek committed
370
371
372
373
374
375
376
377
						on:click={async () => {
							await chats.set(await getChatList(localStorage.token));
						}}
					>
						all
					</button>
					{#each $tags as tag}
						<button
378
							class="px-2.5 text-xs font-medium bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
Timothy J. Baek's avatar
Timothy J. Baek committed
379
							on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
380
381
382
383
384
385
								let chatIds = await getChatListByTagName(localStorage.token, tag.name);
								if (chatIds.length === 0) {
									await tags.set(await getAllChatTags(localStorage.token));
									chatIds = await getChatList(localStorage.token);
								}
								await chats.set(chatIds);
Timothy J. Baek's avatar
Timothy J. Baek committed
386
387
388
389
390
391
392
393
							}}
						>
							{tag.name}
						</button>
					{/each}
				</div>
			{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
394
			<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto">
395
396
				{#each $chats.filter((chat) => {
					if (search === '') {
397
398
						return true;
					} else {
399
						let title = chat.title.toLowerCase();
Timothy J. Baek's avatar
Timothy J. Baek committed
400
						const query = search.toLowerCase();
401

Henry Holloway's avatar
Henry Holloway committed
402
403
404
						let contentMatches = false;
						// Access the messages within chat.chat.messages
						if (chat.chat && chat.chat.messages && Array.isArray(chat.chat.messages)) {
Timothy J. Baek's avatar
Timothy J. Baek committed
405
							contentMatches = chat.chat.messages.some((message) => {
Henry Holloway's avatar
Henry Holloway committed
406
407
408
								// Check if message.content exists and includes the search query
								return message.content && message.content.toLowerCase().includes(query);
							});
409
						}
Henry Holloway's avatar
Henry Holloway committed
410
411

						return title.includes(query) || contentMatches;
412
					}
413
				}) as chat, i}
Timothy J. Baek's avatar
Timothy J. Baek committed
414
					<div class=" w-full pr-2 relative group">
Timothy J. Baek's avatar
Timothy J. Baek committed
415
416
						{#if chatTitleEditId === chat.id}
							<div
Timothy J. Baek's avatar
Timothy J. Baek committed
417
418
419
								class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
								chat.id === chatTitleEditId ||
								chat.id === chatDeleteId
420
									? 'bg-gray-300 dark:bg-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
421
									: chat.id === selectedChatId
422
423
									? 'bg-gray-100 dark:bg-gray-950'
									: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'}  whitespace-nowrap text-ellipsis"
Timothy J. Baek's avatar
Timothy J. Baek committed
424
425
426
427
428
							>
								<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
							</div>
						{:else}
							<a
Timothy J. Baek's avatar
Timothy J. Baek committed
429
430
431
								class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
								chat.id === chatTitleEditId ||
								chat.id === chatDeleteId
432
									? 'bg-gray-300 dark:bg-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
433
									: chat.id === selectedChatId
434
435
									? 'bg-gray-100 dark:bg-gray-950'
									: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'}  whitespace-nowrap text-ellipsis"
Timothy J. Baek's avatar
Timothy J. Baek committed
436
								href="/c/{chat.id}"
437
								on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
438
									selectedChatId = chat.id;
439
440
441
442
									if (window.innerWidth < 1024) {
										show = false;
									}
								}}
Timothy J. Baek's avatar
Timothy J. Baek committed
443
								draggable="false"
Timothy J. Baek's avatar
Timothy J. Baek committed
444
445
							>
								<div class=" flex self-center flex-1 w-full">
Timothy J. Baek's avatar
Timothy J. Baek committed
446
									<div class=" text-left self-center overflow-hidden w-full h-[20px]">
447
										{chat.title}
Timothy J. Baek's avatar
Timothy J. Baek committed
448
									</div>
449
								</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
450
451
							</a>
						{/if}
452

Timothy J. Baek's avatar
Timothy J. Baek committed
453
						<div
Timothy J. Baek's avatar
Timothy J. Baek committed
454
455
							class="
							
Timothy J. Baek's avatar
Timothy J. Baek committed
456
							{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
457
								? 'from-gray-300 dark:from-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
458
								: chat.id === selectedChatId
459
460
								? 'from-gray-100 dark:from-gray-950'
								: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
Timothy J. Baek's avatar
Timothy J. Baek committed
461
462
463
								absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
								
								  to-transparent"
Timothy J. Baek's avatar
Timothy J. Baek committed
464
465
466
467
						>
							{#if chatTitleEditId === chat.id}
								<div class="flex self-center space-x-1.5 z-10">
									<button
468
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
469
470
471
472
473
474
475
476
477
478
479
										on:click={() => {
											editChatTitle(chat.id, chatTitle);
											chatTitleEditId = null;
											chatTitle = '';
										}}
									>
										<svg
											xmlns="http://www.w3.org/2000/svg"
											viewBox="0 0 20 20"
											fill="currentColor"
											class="w-4 h-4"
480
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
481
482
483
484
485
486
487
488
											<path
												fill-rule="evenodd"
												d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
												clip-rule="evenodd"
											/>
										</svg>
									</button>
									<button
489
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
490
491
492
493
494
495
496
497
498
499
										on:click={() => {
											chatTitleEditId = null;
											chatTitle = '';
										}}
									>
										<svg
											xmlns="http://www.w3.org/2000/svg"
											viewBox="0 0 20 20"
											fill="currentColor"
											class="w-4 h-4"
500
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
501
502
503
504
505
506
507
508
509
											<path
												d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
											/>
										</svg>
									</button>
								</div>
							{:else if chatDeleteId === chat.id}
								<div class="flex self-center space-x-1.5 z-10">
									<button
510
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
511
512
513
514
515
516
517
518
519
										on:click={() => {
											deleteChat(chat.id);
										}}
									>
										<svg
											xmlns="http://www.w3.org/2000/svg"
											viewBox="0 0 20 20"
											fill="currentColor"
											class="w-4 h-4"
520
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
521
522
523
524
525
526
527
528
											<path
												fill-rule="evenodd"
												d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
												clip-rule="evenodd"
											/>
										</svg>
									</button>
									<button
529
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
530
531
532
533
534
535
536
537
538
										on:click={() => {
											chatDeleteId = null;
										}}
									>
										<svg
											xmlns="http://www.w3.org/2000/svg"
											viewBox="0 0 20 20"
											fill="currentColor"
											class="w-4 h-4"
539
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
540
541
542
543
544
545
546
547
											<path
												d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
											/>
										</svg>
									</button>
								</div>
							{:else}
								<div class="flex self-center space-x-1.5 z-10">
Timothy J. Baek's avatar
Timothy J. Baek committed
548
									<ChatMenu
Timothy J. Baek's avatar
Timothy J. Baek committed
549
										chatId={chat.id}
Timothy J. Baek's avatar
Timothy J. Baek committed
550
										renameHandler={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
551
552
553
											chatTitle = chat.title;
											chatTitleEditId = chat.id;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
554
										deleteHandler={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
555
556
											chatDeleteId = chat.id;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
557
558
559
										onClose={() => {
											selectedChatId = null;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
560
									>
561
										<button
Timothy J. Baek's avatar
Timothy J. Baek committed
562
											aria-label="Chat Menu"
563
											class=" self-center dark:hover:text-white transition"
564
											on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
565
												selectedChatId = chat.id;
566
											}}
567
										>
568
569
											<svg
												xmlns="http://www.w3.org/2000/svg"
Timothy J. Baek's avatar
Timothy J. Baek committed
570
571
												viewBox="0 0 16 16"
												fill="currentColor"
572
573
574
												class="w-4 h-4"
											>
												<path
Timothy J. Baek's avatar
Timothy J. Baek committed
575
													d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
576
577
578
												/>
											</svg>
										</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
579
									</ChatMenu>
Timothy J. Baek's avatar
Timothy J. Baek committed
580
581
582
								</div>
							{/if}
						</div>
583
584
585
					</div>
				{/each}
			</div>
586
587
588
		</div>

		<div class="px-2.5">
Timothy J. Baek's avatar
Timothy J. Baek committed
589
			<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
590
591
592
593

			<div class="flex flex-col">
				{#if $user !== undefined}
					<button
594
						class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-900 transition"
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
						on:click={() => {
							showDropdown = !showDropdown;
						}}
					>
						<div class=" self-center mr-3">
							<img
								src={$user.profile_image_url}
								class=" max-w-[30px] object-cover rounded-full"
								alt="User profile"
							/>
						</div>
						<div class=" self-center font-semibold">{$user.name}</div>
					</button>

					{#if showDropdown}
						<div
							id="dropdownDots"
612
							class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px] bg-white dark:bg-gray-900"
Timothy J. Baek's avatar
Timothy J. Baek committed
613
							transition:fade|slide={{ duration: 100 }}
614
615
616
617
						>
							<div class="py-2 w-full">
								{#if $user.role === 'admin'}
									<button
Jannik Streidl's avatar
Jannik Streidl committed
618
										class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
619
620
										on:click={() => {
											goto('/admin');
621
											showDropdown = false;
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
										}}
									>
										<div class=" self-center mr-3">
											<svg
												xmlns="http://www.w3.org/2000/svg"
												fill="none"
												viewBox="0 0 24 24"
												stroke-width="1.5"
												stroke="currentColor"
												class="w-5 h-5"
											>
												<path
													stroke-linecap="round"
													stroke-linejoin="round"
													d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
												/>
											</svg>
										</div>
640
										<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
641
									</button>
642
643

									<button
Jannik Streidl's avatar
Jannik Streidl committed
644
										class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
										on:click={() => {
											goto('/playground');
											showDropdown = false;
										}}
									>
										<div class=" self-center mr-3">
											<svg
												xmlns="http://www.w3.org/2000/svg"
												fill="none"
												viewBox="0 0 24 24"
												stroke-width="1.5"
												stroke="currentColor"
												class="w-5 h-5"
											>
												<path
													stroke-linecap="round"
													stroke-linejoin="round"
													d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
												/>
											</svg>
										</div>
666
										<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
667
									</button>
668
669
670
								{/if}

								<button
Jannik Streidl's avatar
Jannik Streidl committed
671
									class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
672
673
									on:click={async () => {
										await showSettings.set(true);
674
										showDropdown = false;
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
									}}
								>
									<div class=" self-center mr-3">
										<svg
											xmlns="http://www.w3.org/2000/svg"
											fill="none"
											viewBox="0 0 24 24"
											stroke-width="1.5"
											stroke="currentColor"
											class="w-5 h-5"
										>
											<path
												stroke-linecap="round"
												stroke-linejoin="round"
												d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
											/>
											<path
												stroke-linecap="round"
												stroke-linejoin="round"
												d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
											/>
										</svg>
									</div>
698
									<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
699
700
701
								</button>
							</div>

702
							<hr class=" dark:border-gray-700 m-0 p-0" />
703
704
705

							<div class="py-2 w-full">
								<button
Jannik Streidl's avatar
Jannik Streidl committed
706
									class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
707
708
709
									on:click={() => {
										localStorage.removeItem('token');
										location.href = '/auth';
710
										showDropdown = false;
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
									}}
								>
									<div class=" self-center mr-3">
										<svg
											xmlns="http://www.w3.org/2000/svg"
											viewBox="0 0 20 20"
											fill="currentColor"
											class="w-5 h-5"
										>
											<path
												fill-rule="evenodd"
												d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z"
												clip-rule="evenodd"
											/>
											<path
												fill-rule="evenodd"
												d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z"
												clip-rule="evenodd"
											/>
										</svg>
									</div>
732
									<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
733
734
735
736
737
738
739
740
741
742
								</button>
							</div>
						</div>
					{/if}
				{/if}
			</div>
		</div>
	</div>

	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
743
		id="sidebar-handle"
Timothy J. Baek's avatar
Timothy J. Baek committed
744
		class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
745
	>
Jannik Streidl's avatar
Jannik Streidl committed
746
747
748
749
750
		<Tooltip
			placement="right"
			content={`${show ? $i18n.t('Close') : $i18n.t('Open')} ${$i18n.t('sidebar')}`}
			touch={false}
		>
751
752
753
754
755
756
757
758
			<button
				id="sidebar-toggle-button"
				class=" group"
				on:click={() => {
					show = !show;
				}}
				><span class="" data-state="closed"
					><div
759
						class="flex h-[72px] w-8 items-center justify-center opacity-50 group-hover:opacity-100 transition"
760
761
762
763
764
765
766
767
768
769
770
771
772
					>
						<div class="flex h-6 w-6 flex-col items-center">
							<div
								class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[0.15rem] {show
									? 'group-hover:rotate-[15deg]'
									: 'group-hover:rotate-[-15deg]'}"
							/>
							<div
								class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[-0.15rem] {show
									? 'group-hover:rotate-[-15deg]'
									: 'group-hover:rotate-[15deg]'}"
							/>
						</div>
773
					</div>
774
775
776
				</span>
			</button>
		</Tooltip>
777
778
	</div>
</div>