Sidebar.svelte 24.7 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
65
66
67
68
69
70
71
72

		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;
				}
			}
		}

		document.addEventListener('touchstart', (e) => {
			touchstartX = e.changedTouches[0].screenX;
		});

		document.addEventListener('touchend', (e) => {
			touchendX = e.changedTouches[0].screenX;
			checkDirection();
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
	});

	// 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);
	};
89
90
91
92
93
94

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

	const editChatTitle = async (id, _title) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
95
96
97
98
99
100
101
102
103
104
		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));
		}
105
106
107
	};

	const deleteChat = async (id) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
108
109
110
		const res = await deleteChatById(localStorage.token, id).catch((error) => {
			toast.error(error);
			chatDeleteId = null;
111

Timothy J. Baek's avatar
Timothy J. Baek committed
112
113
114
115
			return null;
		});

		if (res) {
Timothy J. Baek's avatar
Timothy J. Baek committed
116
117
118
119
			if ($chatId === id) {
				goto('/');
			}

Timothy J. Baek's avatar
Timothy J. Baek committed
120
121
			await chats.set(await getChatList(localStorage.token));
		}
122
	};
123
124
125
126
127
128

	const saveSettings = async (updated) => {
		await settings.set({ ...$settings, ...updated });
		localStorage.setItem('settings', JSON.stringify($settings));
		location.href = '/';
	};
129
130
131
132
</script>

<div
	bind:this={navElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
133
	class="h-screen max-h-[100dvh] min-h-screen {show
Timothy J. Baek's avatar
Timothy J. Baek committed
134
		? 'lg:relative w-[260px]'
Timothy J. Baek's avatar
Timothy J. Baek committed
135
		: '-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
136
137
        "
>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
138
	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
139
		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
140
141
142
			? ''
			: 'invisible'}"
	>
Timothy J. Baek's avatar
Timothy J. Baek committed
143
		<div class="px-2 flex justify-center space-x-2">
144
			<a
Timothy J. Baek's avatar
Timothy J. Baek committed
145
				id="sidebar-new-chat-button"
146
				class="flex-grow flex justify-between rounded-xl px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
147
				href="/"
Timothy J. Baek's avatar
Timothy J. Baek committed
148
				on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
149
150
					selectedChatId = null;

151
					await goto('/');
152
					const newChatButton = document.getElementById('new-chat-button');
153
154
155
					setTimeout(() => {
						newChatButton?.click();
					}, 0);
156
157
158
				}}
			>
				<div class="flex self-center">
Timothy J. Baek's avatar
Timothy J. Baek committed
159
					<div class="self-center mr-1.5">
160
161
						<img
							src="{WEBUI_BASE_URL}/static/favicon.png"
162
							class=" size-6 -translate-x-1.5 rounded-full"
163
164
							alt="logo"
						/>
165
166
					</div>

167
					<div class=" self-center font-medium text-sm">{$i18n.t('New Chat')}</div>
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
				</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>
185
			</a>
186
187
		</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
188
		{#if $user?.role === 'admin'}
Timothy J. Baek's avatar
Timothy J. Baek committed
189
			<div class="px-2 flex justify-center mt-0.5">
190
				<a
191
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
192
					href="/modelfiles"
Timothy J. Baek's avatar
Timothy J. Baek committed
193
194
195
196
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
197
198
199
200
201
202
				>
					<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
203
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
204
205
206
207
208
209
							stroke="currentColor"
							class="w-4 h-4"
						>
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
Timothy J. Baek's avatar
Timothy J. Baek committed
210
								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
211
212
213
							/>
						</svg>
					</div>
214

Timothy J. Baek's avatar
Timothy J. Baek committed
215
					<div class="flex self-center">
216
						<div class=" self-center font-medium text-sm">{$i18n.t('Modelfiles')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
217
					</div>
218
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
219
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
220

Timothy J. Baek's avatar
Timothy J. Baek committed
221
			<div class="px-2 flex justify-center">
222
				<a
223
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
224
					href="/prompts"
Timothy J. Baek's avatar
Timothy J. Baek committed
225
226
227
228
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
229
230
231
232
				>
					<div class="self-center">
						<svg
							xmlns="http://www.w3.org/2000/svg"
Timothy J. Baek's avatar
Timothy J. Baek committed
233
234
							fill="none"
							viewBox="0 0 24 24"
Timothy J. Baek's avatar
Timothy J. Baek committed
235
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
236
							stroke="currentColor"
Timothy J. Baek's avatar
Timothy J. Baek committed
237
238
239
							class="w-4 h-4"
						>
							<path
Timothy J. Baek's avatar
Timothy J. Baek committed
240
241
242
								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
243
244
245
246
247
							/>
						</svg>
					</div>

					<div class="flex self-center">
248
						<div class=" self-center font-medium text-sm">{$i18n.t('Prompts')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
249
					</div>
250
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
251
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
252

Timothy J. Baek's avatar
Timothy J. Baek committed
253
			<div class="px-2 flex justify-center mb-1">
254
				<a
255
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
256
					href="/documents"
Timothy J. Baek's avatar
Timothy J. Baek committed
257
258
259
260
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
261
262
263
264
265
266
				>
					<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
267
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
268
269
270
271
272
273
274
275
276
277
278
279
							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">
280
						<div class=" self-center font-medium text-sm">{$i18n.t('Documents')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
281
					</div>
282
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
283
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
284
		{/if}
285

286
287
		<div class="relative flex flex-col flex-1 overflow-y-auto">
			{#if !($settings.saveChatHistory ?? true)}
Timothy J. Baek's avatar
Timothy J. Baek committed
288
				<div class="absolute z-40 w-full h-full bg-gray-50/90 dark:bg-black/90 flex justify-center">
289
					<div class=" text-left px-5 py-2">
290
						<div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
291
						<div class="text-xs mt-2">
Jannik Streidl's avatar
Jannik Streidl committed
292
293
294
295
296
							{$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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
							>
						</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
323
								<div>{$i18n.t('Enable Chat History')}</div>
324
325
326
							</button>
						</div>
					</div>
327
				</div>
328
			{/if}
329

Timothy J. Baek's avatar
Timothy J. Baek committed
330
			<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
331
				<div class="flex w-full" id="chat-search">
332
					<div class="self-center pl-3 py-2 rounded-l-xl bg-white dark:bg-gray-950">
333
334
335
336
337
338
339
340
341
342
343
344
345
						<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>
346

347
					<input
348
						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
349
						placeholder={$i18n.t('Search')}
350
						bind:value={search}
Timothy J. Baek's avatar
Timothy J. Baek committed
351
352
353
						on:focus={() => {
							enrichChatsWithContent($chats);
						}}
354
355
356
					/>
				</div>
			</div>
357

Timothy J. Baek's avatar
Timothy J. Baek committed
358
359
360
			{#if $tags.length > 0}
				<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
					<button
361
						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
362
363
364
365
366
367
368
369
						on:click={async () => {
							await chats.set(await getChatList(localStorage.token));
						}}
					>
						all
					</button>
					{#each $tags as tag}
						<button
370
							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
371
							on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
372
373
374
375
376
377
								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
378
379
380
381
382
383
384
385
							}}
						>
							{tag.name}
						</button>
					{/each}
				</div>
			{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
386
			<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto">
387
388
				{#each $chats.filter((chat) => {
					if (search === '') {
389
390
						return true;
					} else {
391
						let title = chat.title.toLowerCase();
Timothy J. Baek's avatar
Timothy J. Baek committed
392
						const query = search.toLowerCase();
393

Henry Holloway's avatar
Henry Holloway committed
394
395
396
						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
397
							contentMatches = chat.chat.messages.some((message) => {
Henry Holloway's avatar
Henry Holloway committed
398
399
400
								// Check if message.content exists and includes the search query
								return message.content && message.content.toLowerCase().includes(query);
							});
401
						}
Henry Holloway's avatar
Henry Holloway committed
402
403

						return title.includes(query) || contentMatches;
404
					}
405
				}) as chat, i}
Timothy J. Baek's avatar
Timothy J. Baek committed
406
					<div class=" w-full pr-2 relative group">
Timothy J. Baek's avatar
Timothy J. Baek committed
407
408
						{#if chatTitleEditId === chat.id}
							<div
Timothy J. Baek's avatar
Timothy J. Baek committed
409
410
411
								class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
								chat.id === chatTitleEditId ||
								chat.id === chatDeleteId
412
									? 'bg-gray-300 dark:bg-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
413
									: chat.id === selectedChatId
414
415
									? '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
416
417
418
419
420
							>
								<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
421
422
423
								class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
								chat.id === chatTitleEditId ||
								chat.id === chatDeleteId
424
									? 'bg-gray-300 dark:bg-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
425
									: chat.id === selectedChatId
426
427
									? '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
428
								href="/c/{chat.id}"
429
								on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
430
									selectedChatId = chat.id;
431
432
433
434
									if (window.innerWidth < 1024) {
										show = false;
									}
								}}
Timothy J. Baek's avatar
Timothy J. Baek committed
435
								draggable="false"
Timothy J. Baek's avatar
Timothy J. Baek committed
436
437
							>
								<div class=" flex self-center flex-1 w-full">
Timothy J. Baek's avatar
Timothy J. Baek committed
438
									<div class=" text-left self-center overflow-hidden w-full h-[20px]">
439
										{chat.title}
Timothy J. Baek's avatar
Timothy J. Baek committed
440
									</div>
441
								</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
442
443
							</a>
						{/if}
444

Timothy J. Baek's avatar
Timothy J. Baek committed
445
						<div
Timothy J. Baek's avatar
Timothy J. Baek committed
446
447
							class="
							
Timothy J. Baek's avatar
Timothy J. Baek committed
448
							{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
449
								? 'from-gray-300 dark:from-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
450
								: chat.id === selectedChatId
451
452
								? '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
453
454
455
								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
456
457
458
459
						>
							{#if chatTitleEditId === chat.id}
								<div class="flex self-center space-x-1.5 z-10">
									<button
460
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
461
462
463
464
465
466
467
468
469
470
471
										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"
472
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
473
474
475
476
477
478
479
480
											<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
481
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
482
483
484
485
486
487
488
489
490
491
										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"
492
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
493
494
495
496
497
498
499
500
501
											<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
502
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
503
504
505
506
507
508
509
510
511
										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"
512
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
513
514
515
516
517
518
519
520
											<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
521
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
522
523
524
525
526
527
528
529
530
										on:click={() => {
											chatDeleteId = null;
										}}
									>
										<svg
											xmlns="http://www.w3.org/2000/svg"
											viewBox="0 0 20 20"
											fill="currentColor"
											class="w-4 h-4"
531
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
532
533
534
535
536
537
538
539
											<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
540
541
									<ChatMenu
										renameHandler={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
542
543
544
											chatTitle = chat.title;
											chatTitleEditId = chat.id;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
545
										deleteHandler={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
546
547
											chatDeleteId = chat.id;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
548
549
550
										onClose={() => {
											selectedChatId = null;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
551
									>
552
										<button
Timothy J. Baek's avatar
Timothy J. Baek committed
553
											aria-label="Chat Menu"
554
											class=" self-center dark:hover:text-white transition"
555
											on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
556
												selectedChatId = chat.id;
557
											}}
558
										>
559
560
											<svg
												xmlns="http://www.w3.org/2000/svg"
Timothy J. Baek's avatar
Timothy J. Baek committed
561
562
												viewBox="0 0 16 16"
												fill="currentColor"
563
564
565
												class="w-4 h-4"
											>
												<path
Timothy J. Baek's avatar
Timothy J. Baek committed
566
													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"
567
568
569
												/>
											</svg>
										</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
570
									</ChatMenu>
Timothy J. Baek's avatar
Timothy J. Baek committed
571
572
573
								</div>
							{/if}
						</div>
574
575
576
					</div>
				{/each}
			</div>
577
578
579
		</div>

		<div class="px-2.5">
Timothy J. Baek's avatar
Timothy J. Baek committed
580
			<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
581
582
583
584

			<div class="flex flex-col">
				{#if $user !== undefined}
					<button
585
						class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-900 transition"
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
						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"
603
							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
604
							transition:fade|slide={{ duration: 100 }}
605
606
607
608
						>
							<div class="py-2 w-full">
								{#if $user.role === 'admin'}
									<button
Jannik Streidl's avatar
Jannik Streidl committed
609
										class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
610
611
										on:click={() => {
											goto('/admin');
612
											showDropdown = false;
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
										}}
									>
										<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>
631
										<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
632
									</button>
633
634

									<button
Jannik Streidl's avatar
Jannik Streidl committed
635
										class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
										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>
657
										<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
658
									</button>
659
660
661
								{/if}

								<button
Jannik Streidl's avatar
Jannik Streidl committed
662
									class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
663
664
									on:click={async () => {
										await showSettings.set(true);
665
										showDropdown = false;
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
									}}
								>
									<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>
689
									<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
690
691
692
								</button>
							</div>

693
							<hr class=" dark:border-gray-700 m-0 p-0" />
694
695
696

							<div class="py-2 w-full">
								<button
Jannik Streidl's avatar
Jannik Streidl committed
697
									class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
698
699
700
									on:click={() => {
										localStorage.removeItem('token');
										location.href = '/auth';
701
										showDropdown = false;
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
									}}
								>
									<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>
723
									<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
724
725
726
727
728
729
730
731
732
733
								</button>
							</div>
						</div>
					{/if}
				{/if}
			</div>
		</div>
	</div>

	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
734
		id="sidebar-handle"
Timothy J. Baek's avatar
Timothy J. Baek committed
735
		class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
736
	>
Jannik Streidl's avatar
Jannik Streidl committed
737
738
739
740
741
		<Tooltip
			placement="right"
			content={`${show ? $i18n.t('Close') : $i18n.t('Open')} ${$i18n.t('sidebar')}`}
			touch={false}
		>
742
743
744
745
746
747
748
749
			<button
				id="sidebar-toggle-button"
				class=" group"
				on:click={() => {
					show = !show;
				}}
				><span class="" data-state="closed"
					><div
750
						class="flex h-[72px] w-8 items-center justify-center opacity-50 group-hover:opacity-100 transition"
751
752
753
754
755
756
757
758
759
760
761
762
763
					>
						<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>
764
					</div>
765
766
767
				</span>
			</button>
		</Tooltip>
768
769
	</div>
</div>