Sidebar.svelte 27 KB
Newer Older
1
<script lang="ts">
Timothy J. Baek's avatar
Timothy J. Baek committed
2
3
	import { goto } from '$app/navigation';
	import { user, chats, settings, showSettings, chatId, tags, showSidebar } from '$lib/stores';
4
	import { onMount, getContext } from 'svelte';
5
6
7

	const i18n = getContext('i18n');

Timothy J. Baek's avatar
Timothy J. Baek committed
8
9
10
	import {
		deleteChatById,
		getChatList,
Henry Holloway's avatar
Henry Holloway committed
11
		getChatById,
Timothy J. Baek's avatar
Timothy J. Baek committed
12
		getChatListByTagName,
Timothy J. Baek's avatar
Timothy J. Baek committed
13
		updateChatById,
Timothy J. Baek's avatar
Timothy J. Baek committed
14
15
		getAllChatTags,
		archiveChatById
Timothy J. Baek's avatar
Timothy J. Baek committed
16
	} from '$lib/apis/chats';
Jannik Streidl's avatar
Jannik Streidl committed
17
	import { toast } from 'svelte-sonner';
Timothy J. Baek's avatar
Timothy J. Baek committed
18
	import { fade, slide } from 'svelte/transition';
19
	import { WEBUI_BASE_URL } from '$lib/constants';
20
	import Tooltip from '../common/Tooltip.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
21
	import ChatMenu from './Sidebar/ChatMenu.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
22
	import ShareChatModal from '../chat/ShareChatModal.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
23
	import ArchiveBox from '../icons/ArchiveBox.svelte';
24
	import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
25

26
	const BREAKPOINT = 1024;
Timothy J. Baek's avatar
Timothy J. Baek committed
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 shareChatId = null;

Timothy J. Baek's avatar
Timothy J. Baek committed
36
37
	let selectedChatId = null;

38
39
	let chatDeleteId = null;
	let chatTitleEditId = null;
40
41
	let chatTitle = '';

42
	let showArchivedChatsModal = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
43
	let showShareChatModal = false;
44
	let showDropdown = false;
45
	let isEditing = false;
46
47

	onMount(async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
48
		showSidebar.set(window.innerWidth > BREAKPOINT);
Timothy J. Baek's avatar
Timothy J. Baek committed
49
		await chats.set(await getChatList(localStorage.token));
Timothy J. Baek's avatar
Timothy J. Baek committed
50

51
52
		let touchstart;
		let touchend;
Timothy J. Baek's avatar
Timothy J. Baek committed
53
54
55

		function checkDirection() {
			const screenWidth = window.innerWidth;
56
57
58
			const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
			if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 4) {
				if (touchend.screenX < touchstart.screenX) {
Timothy J. Baek's avatar
Timothy J. Baek committed
59
					showSidebar.set(false);
Timothy J. Baek's avatar
Timothy J. Baek committed
60
				}
61
				if (touchend.screenX > touchstart.screenX) {
Timothy J. Baek's avatar
Timothy J. Baek committed
62
					showSidebar.set(true);
Timothy J. Baek's avatar
Timothy J. Baek committed
63
64
65
66
				}
			}
		}

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
67
		const onTouchStart = (e) => {
68
69
			touchstart = e.changedTouches[0];
			console.log(touchstart.clientX);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
70
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
71

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
72
		const onTouchEnd = (e) => {
73
			touchend = e.changedTouches[0];
Timothy J. Baek's avatar
Timothy J. Baek committed
74
			checkDirection();
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
75
76
		};

77
		const onResize = () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
78
79
			if ($showSidebar && window.innerWidth < BREAKPOINT) {
				showSidebar.set(false);
80
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
81
		};
82

83
84
		window.addEventListener('touchstart', onTouchStart);
		window.addEventListener('touchend', onTouchEnd);
85
		window.addEventListener('resize', onResize);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
86
87

		return () => {
88
89
90
			window.removeEventListener('touchstart', onTouchStart);
			window.removeEventListener('touchend', onTouchEnd);
			window.removeEventListener('resize', onResize);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
91
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
	});

	// 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);
	};
108
109
110
111
112
113

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

	const editChatTitle = async (id, _title) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
114
115
116
117
118
119
120
121
122
123
		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));
		}
124
125
126
	};

	const deleteChat = async (id) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
127
128
129
		const res = await deleteChatById(localStorage.token, id).catch((error) => {
			toast.error(error);
			chatDeleteId = null;
130

Timothy J. Baek's avatar
Timothy J. Baek committed
131
132
133
134
			return null;
		});

		if (res) {
Timothy J. Baek's avatar
Timothy J. Baek committed
135
136
137
138
			if ($chatId === id) {
				goto('/');
			}

Timothy J. Baek's avatar
Timothy J. Baek committed
139
140
			await chats.set(await getChatList(localStorage.token));
		}
141
	};
142
143
144
145
146
147

	const saveSettings = async (updated) => {
		await settings.set({ ...$settings, ...updated });
		localStorage.setItem('settings', JSON.stringify($settings));
		location.href = '/';
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
148
149
150
151
152

	const archiveChatHandler = async (id) => {
		await archiveChatById(localStorage.token, id);
		await chats.set(await getChatList(localStorage.token));
	};
153
154
</script>

Timothy J. Baek's avatar
Timothy J. Baek committed
155
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
Timothy J. Baek's avatar
Timothy J. Baek committed
156
157
158
159
160
161
<ArchivedChatsModal
	bind:show={showArchivedChatsModal}
	on:change={async () => {
		await chats.set(await getChatList(localStorage.token));
	}}
/>
Timothy J. Baek's avatar
Timothy J. Baek committed
162

163
164
<div
	bind:this={navElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
165
166
	id="sidebar"
	class="h-screen max-h-[100dvh] min-h-screen {$showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
167
		? 'lg:relative w-[260px]'
Timothy J. Baek's avatar
Timothy J. Baek committed
168
		: '-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
169
        "
Timothy J. Baek's avatar
Timothy J. Baek committed
170
	data-state={$showSidebar}
171
>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
172
	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
173
		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] {$showSidebar
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
174
175
176
			? ''
			: 'invisible'}"
	>
Timothy J. Baek's avatar
Timothy J. Baek committed
177
		<div class="px-2 flex justify-center space-x-2">
178
			<a
Timothy J. Baek's avatar
Timothy J. Baek committed
179
				id="sidebar-new-chat-button"
Timothy J. Baek's avatar
Timothy J. Baek committed
180
				class="flex-grow flex justify-between rounded-xl px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
181
				href="/"
Timothy J. Baek's avatar
Timothy J. Baek committed
182
				on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
183
184
					selectedChatId = null;

185
					await goto('/');
186
					const newChatButton = document.getElementById('new-chat-button');
187
188
189
					setTimeout(() => {
						newChatButton?.click();
					}, 0);
190
191
192
				}}
			>
				<div class="flex self-center">
Timothy J. Baek's avatar
Timothy J. Baek committed
193
					<div class="self-center mr-1.5">
194
195
						<img
							src="{WEBUI_BASE_URL}/static/favicon.png"
196
							class=" size-6 -translate-x-1.5 rounded-full"
197
198
							alt="logo"
						/>
199
200
					</div>

201
					<div class=" self-center font-medium text-sm">{$i18n.t('New Chat')}</div>
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
				</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>
219
			</a>
220
221
		</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
222
		{#if $user?.role === 'admin'}
Timothy J. Baek's avatar
Timothy J. Baek committed
223
			<div class="px-2 flex justify-center mt-0.5">
224
				<a
Timothy J. Baek's avatar
Timothy J. Baek committed
225
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
226
					href="/modelfiles"
Timothy J. Baek's avatar
Timothy J. Baek committed
227
228
229
230
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
231
232
233
234
235
236
				>
					<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
237
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
238
239
240
241
242
243
							stroke="currentColor"
							class="w-4 h-4"
						>
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
Timothy J. Baek's avatar
Timothy J. Baek committed
244
								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
245
246
247
							/>
						</svg>
					</div>
248

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

Timothy J. Baek's avatar
Timothy J. Baek committed
255
			<div class="px-2 flex justify-center">
256
				<a
Timothy J. Baek's avatar
Timothy J. Baek committed
257
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
258
					href="/prompts"
Timothy J. Baek's avatar
Timothy J. Baek committed
259
260
261
262
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
263
264
265
266
				>
					<div class="self-center">
						<svg
							xmlns="http://www.w3.org/2000/svg"
Timothy J. Baek's avatar
Timothy J. Baek committed
267
268
							fill="none"
							viewBox="0 0 24 24"
Timothy J. Baek's avatar
Timothy J. Baek committed
269
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
270
							stroke="currentColor"
Timothy J. Baek's avatar
Timothy J. Baek committed
271
272
273
							class="w-4 h-4"
						>
							<path
Timothy J. Baek's avatar
Timothy J. Baek committed
274
275
276
								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
277
278
279
280
281
							/>
						</svg>
					</div>

					<div class="flex self-center">
282
						<div class=" self-center font-medium text-sm">{$i18n.t('Prompts')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
283
					</div>
284
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
285
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
286

Timothy J. Baek's avatar
Timothy J. Baek committed
287
			<div class="px-2 flex justify-center mb-1">
288
				<a
Timothy J. Baek's avatar
Timothy J. Baek committed
289
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
290
					href="/documents"
Timothy J. Baek's avatar
Timothy J. Baek committed
291
292
293
294
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
295
296
297
298
299
300
				>
					<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
301
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
302
303
304
305
306
307
308
309
310
311
312
313
							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">
314
						<div class=" self-center font-medium text-sm">{$i18n.t('Documents')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
315
					</div>
316
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
317
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
318
		{/if}
319

320
321
		<div class="relative flex flex-col flex-1 overflow-y-auto">
			{#if !($settings.saveChatHistory ?? true)}
Timothy J. Baek's avatar
Timothy J. Baek committed
322
				<div class="absolute z-40 w-full h-full bg-gray-50/90 dark:bg-black/90 flex justify-center">
323
					<div class=" text-left px-5 py-2">
324
						<div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
325
						<div class="text-xs mt-2">
Jannik Streidl's avatar
Jannik Streidl committed
326
327
328
329
330
							{$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
331
332
333
334
335
							>
						</div>

						<div class="mt-3">
							<button
Timothy J. Baek's avatar
Timothy J. Baek committed
336
								class="flex justify-center items-center space-x-1.5 px-3 py-2.5 rounded-lg text-xs bg-gray-100 hover:bg-gray-200 transition text-gray-800 font-medium w-full"
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
								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
357
								<div>{$i18n.t('Enable Chat History')}</div>
358
359
360
							</button>
						</div>
					</div>
361
				</div>
362
			{/if}
363

Timothy J. Baek's avatar
Timothy J. Baek committed
364
			<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
365
				<div class="flex w-full" id="chat-search">
366
					<div class="self-center pl-3 py-2 rounded-l-xl bg-white dark:bg-gray-950">
367
368
369
370
371
372
373
374
375
376
377
378
379
						<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>
380

381
					<input
382
						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
383
						placeholder={$i18n.t('Search')}
384
						bind:value={search}
Timothy J. Baek's avatar
Timothy J. Baek committed
385
386
387
						on:focus={() => {
							enrichChatsWithContent($chats);
						}}
388
389
390
					/>
				</div>
			</div>
391

Timothy J. Baek's avatar
Timothy J. Baek committed
392
393
394
			{#if $tags.length > 0}
				<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
					<button
395
						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
396
397
398
399
400
401
402
403
						on:click={async () => {
							await chats.set(await getChatList(localStorage.token));
						}}
					>
						all
					</button>
					{#each $tags as tag}
						<button
404
							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
405
							on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
406
407
408
409
410
411
								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
412
413
414
415
416
417
418
419
							}}
						>
							{tag.name}
						</button>
					{/each}
				</div>
			{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
420
			<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-none">
421
422
				{#each $chats.filter((chat) => {
					if (search === '') {
423
424
						return true;
					} else {
425
						let title = chat.title.toLowerCase();
Timothy J. Baek's avatar
Timothy J. Baek committed
426
						const query = search.toLowerCase();
427

Henry Holloway's avatar
Henry Holloway committed
428
429
430
						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
431
							contentMatches = chat.chat.messages.some((message) => {
Henry Holloway's avatar
Henry Holloway committed
432
433
434
								// Check if message.content exists and includes the search query
								return message.content && message.content.toLowerCase().includes(query);
							});
435
						}
Henry Holloway's avatar
Henry Holloway committed
436
437

						return title.includes(query) || contentMatches;
438
					}
439
				}) as chat, i}
Timothy J. Baek's avatar
Timothy J. Baek committed
440
					<div class=" w-full pr-2 relative group">
Timothy J. Baek's avatar
Timothy J. Baek committed
441
442
						{#if chatTitleEditId === chat.id}
							<div
Timothy J. Baek's avatar
Timothy J. Baek committed
443
444
445
								class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
								chat.id === chatTitleEditId ||
								chat.id === chatDeleteId
Timothy J. Baek's avatar
Timothy J. Baek committed
446
									? 'bg-gray-200 dark:bg-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
447
									: chat.id === selectedChatId
448
449
									? '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
450
451
452
453
454
							>
								<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
455
456
457
								class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
								chat.id === chatTitleEditId ||
								chat.id === chatDeleteId
Timothy J. Baek's avatar
Timothy J. Baek committed
458
									? 'bg-gray-200 dark:bg-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
459
									: chat.id === selectedChatId
460
461
									? '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
462
								href="/c/{chat.id}"
463
								on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
464
									selectedChatId = chat.id;
465
									if (window.innerWidth < 1024) {
Timothy J. Baek's avatar
Timothy J. Baek committed
466
										showSidebar.set(false);
467
468
									}
								}}
Timothy J. Baek's avatar
Timothy J. Baek committed
469
								draggable="false"
Timothy J. Baek's avatar
Timothy J. Baek committed
470
471
							>
								<div class=" flex self-center flex-1 w-full">
Timothy J. Baek's avatar
Timothy J. Baek committed
472
									<div class=" text-left self-center overflow-hidden w-full h-[20px]">
473
										{chat.title}
Timothy J. Baek's avatar
Timothy J. Baek committed
474
									</div>
475
								</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
476
477
							</a>
						{/if}
478

Timothy J. Baek's avatar
Timothy J. Baek committed
479
						<div
Timothy J. Baek's avatar
Timothy J. Baek committed
480
							class="
481

Timothy J. Baek's avatar
Timothy J. Baek committed
482
							{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
Timothy J. Baek's avatar
Timothy J. Baek committed
483
								? 'from-gray-200 dark:from-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
484
								: chat.id === selectedChatId
485
486
								? '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
487
								absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
488

Timothy J. Baek's avatar
Timothy J. Baek committed
489
								  to-transparent"
Timothy J. Baek's avatar
Timothy J. Baek committed
490
491
492
493
						>
							{#if chatTitleEditId === chat.id}
								<div class="flex self-center space-x-1.5 z-10">
									<button
494
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
495
496
497
498
499
500
501
502
503
504
505
										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"
506
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
507
508
509
510
511
512
513
514
											<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
515
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
516
517
518
519
520
521
522
523
524
525
										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"
526
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
527
528
529
530
531
532
533
534
535
											<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
536
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
537
538
539
540
541
542
543
544
545
										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"
546
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
547
548
549
550
551
552
553
554
											<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
555
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
556
557
558
559
560
561
562
563
564
										on:click={() => {
											chatDeleteId = null;
										}}
									>
										<svg
											xmlns="http://www.w3.org/2000/svg"
											viewBox="0 0 20 20"
											fill="currentColor"
											class="w-4 h-4"
565
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
566
567
568
569
570
571
572
											<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}
Timothy J. Baek's avatar
Timothy J. Baek committed
573
								<div class="flex self-center space-x-1 z-10">
Timothy J. Baek's avatar
Timothy J. Baek committed
574
									<ChatMenu
Timothy J. Baek's avatar
Timothy J. Baek committed
575
										chatId={chat.id}
Timothy J. Baek's avatar
Timothy J. Baek committed
576
577
578
579
										shareHandler={() => {
											shareChatId = selectedChatId;
											showShareChatModal = true;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
580
										renameHandler={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
581
582
583
											chatTitle = chat.title;
											chatTitleEditId = chat.id;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
584
										deleteHandler={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
585
586
											chatDeleteId = chat.id;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
587
588
589
										onClose={() => {
											selectedChatId = null;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
590
									>
591
										<button
Timothy J. Baek's avatar
Timothy J. Baek committed
592
											aria-label="Chat Menu"
593
											class=" self-center dark:hover:text-white transition"
594
											on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
595
												selectedChatId = chat.id;
596
											}}
597
										>
598
599
											<svg
												xmlns="http://www.w3.org/2000/svg"
Timothy J. Baek's avatar
Timothy J. Baek committed
600
601
												viewBox="0 0 16 16"
												fill="currentColor"
602
603
604
												class="w-4 h-4"
											>
												<path
Timothy J. Baek's avatar
Timothy J. Baek committed
605
													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"
606
607
608
												/>
											</svg>
										</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
609
									</ChatMenu>
Timothy J. Baek's avatar
Timothy J. Baek committed
610

611
									<Tooltip content={$i18n.t('Archive')}>
Timothy J. Baek's avatar
Timothy J. Baek committed
612
613
614
615
										<button
											aria-label="Archive"
											class=" self-center dark:hover:text-white transition"
											on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
616
												archiveChatHandler(chat.id);
Timothy J. Baek's avatar
Timothy J. Baek committed
617
618
619
620
621
											}}
										>
											<ArchiveBox />
										</button>
									</Tooltip>
Timothy J. Baek's avatar
Timothy J. Baek committed
622
623
624
								</div>
							{/if}
						</div>
625
626
627
					</div>
				{/each}
			</div>
628
629
630
		</div>

		<div class="px-2.5">
Timothy J. Baek's avatar
Timothy J. Baek committed
631
			<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
632
633
634
635

			<div class="flex flex-col">
				{#if $user !== undefined}
					<button
Timothy J. Baek's avatar
Timothy J. Baek committed
636
						class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
						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"
654
							class="absolute z-40 bottom-[70px] rounded-lg shadow w-[240px] bg-white dark:bg-gray-900"
Timothy J. Baek's avatar
Timothy J. Baek committed
655
							transition:fade|slide={{ duration: 100 }}
656
						>
657
							<div class="p-1 py-2 w-full">
658
659
								{#if $user.role === 'admin'}
									<button
660
										class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
661
662
										on:click={() => {
											goto('/admin');
663
											showDropdown = false;
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
										}}
									>
										<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>
682
										<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
683
									</button>
684
685

									<button
686
										class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
										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>
708
										<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
709
									</button>
710
711
712
								{/if}

								<button
713
714
715
716
717
718
719
720
721
722
723
724
725
726
									class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
									on:click={() => {
										showArchivedChatsModal = true;
										showDropdown = false;
									}}
								>
									<div class=" self-center mr-3">
										<ArchiveBox className="size-5" strokeWidth="1.5" />
									</div>
									<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
								</button>

								<button
									class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
727
728
									on:click={async () => {
										await showSettings.set(true);
729
										showDropdown = false;
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
									}}
								>
									<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>
753
									<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
754
755
756
								</button>
							</div>

757
							<hr class=" dark:border-gray-800 m-0 p-0" />
758

759
							<div class="p-1 py-2 w-full">
760
								<button
761
									class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
762
763
764
									on:click={() => {
										localStorage.removeItem('token');
										location.href = '/auth';
765
										showDropdown = false;
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
									}}
								>
									<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>
787
									<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
788
789
790
791
792
793
794
795
796
797
								</button>
							</div>
						</div>
					{/if}
				{/if}
			</div>
		</div>
	</div>

	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
798
		id="sidebar-handle"
Timothy J. Baek's avatar
Timothy J. Baek committed
799
		class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
800
	>
Jannik Streidl's avatar
Jannik Streidl committed
801
802
		<Tooltip
			placement="right"
Timothy J. Baek's avatar
Timothy J. Baek committed
803
			content={`${$showSidebar ? $i18n.t('Close') : $i18n.t('Open')} ${$i18n.t('sidebar')}`}
Jannik Streidl's avatar
Jannik Streidl committed
804
805
			touch={false}
		>
806
807
808
809
			<button
				id="sidebar-toggle-button"
				class=" group"
				on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
810
					showSidebar.set(!$showSidebar);
811
812
813
				}}
				><span class="" data-state="closed"
					><div
814
						class="flex h-[72px] w-8 items-center justify-center opacity-50 group-hover:opacity-100 transition"
815
816
817
818
819
820
821
822
823
824
825
826
827
					>
						<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>
828
					</div>
829
830
831
				</span>
			</button>
		</Tooltip>
832
833
	</div>
</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
834
835
836
837
838
839
840
841
842
843
844

<style>
	.scrollbar-none::-webkit-scrollbar {
		display: none; /* for Chrome, Safari and Opera */
	}

	.scrollbar-none {
		-ms-overflow-style: none; /* IE and Edge */
		scrollbar-width: none; /* Firefox */
	}
</style>