Sidebar.svelte 23.5 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';
Timothy J. Baek's avatar
Timothy J. Baek committed
25
	import UserMenu from './Sidebar/UserMenu.svelte';
26

27
	const BREAKPOINT = 1024;
Timothy J. Baek's avatar
Timothy J. Baek committed
28

29
30
31
	let show = false;
	let navElement;

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

Timothy J. Baek's avatar
Timothy J. Baek committed
35
36
	let shareChatId = null;

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

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

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

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
	let filteredChatList = [];

	$: filteredChatList = $chats.filter((chat) => {
		if (search === '') {
			return true;
		} else {
			let title = chat.title.toLowerCase();
			const query = search.toLowerCase();

			let contentMatches = false;
			// Access the messages within chat.chat.messages
			if (chat.chat && chat.chat.messages && Array.isArray(chat.chat.messages)) {
				contentMatches = chat.chat.messages.some((message) => {
					// Check if message.content exists and includes the search query
					return message.content && message.content.toLowerCase().includes(query);
				});
			}

			return title.includes(query) || contentMatches;
		}
	});

70
	onMount(async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
71
		showSidebar.set(window.innerWidth > BREAKPOINT);
Timothy J. Baek's avatar
Timothy J. Baek committed
72
		await chats.set(await getChatList(localStorage.token));
Timothy J. Baek's avatar
Timothy J. Baek committed
73

74
75
		let touchstart;
		let touchend;
Timothy J. Baek's avatar
Timothy J. Baek committed
76
77
78

		function checkDirection() {
			const screenWidth = window.innerWidth;
79
			const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
Timothy J. Baek's avatar
Timothy J. Baek committed
80
			if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
81
				if (touchend.screenX < touchstart.screenX) {
Timothy J. Baek's avatar
Timothy J. Baek committed
82
					showSidebar.set(false);
Timothy J. Baek's avatar
Timothy J. Baek committed
83
				}
84
				if (touchend.screenX > touchstart.screenX) {
Timothy J. Baek's avatar
Timothy J. Baek committed
85
					showSidebar.set(true);
Timothy J. Baek's avatar
Timothy J. Baek committed
86
87
88
89
				}
			}
		}

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
90
		const onTouchStart = (e) => {
91
92
			touchstart = e.changedTouches[0];
			console.log(touchstart.clientX);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
93
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
94

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
95
		const onTouchEnd = (e) => {
96
			touchend = e.changedTouches[0];
Timothy J. Baek's avatar
Timothy J. Baek committed
97
			checkDirection();
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
98
99
		};

100
		const onResize = () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
101
102
			if ($showSidebar && window.innerWidth < BREAKPOINT) {
				showSidebar.set(false);
103
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
104
		};
105

106
107
		window.addEventListener('touchstart', onTouchStart);
		window.addEventListener('touchend', onTouchEnd);
108
		window.addEventListener('resize', onResize);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
109
110

		return () => {
111
112
113
			window.removeEventListener('touchstart', onTouchStart);
			window.removeEventListener('touchend', onTouchEnd);
			window.removeEventListener('resize', onResize);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
114
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
	});

	// 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);
	};
131
132
133
134
135
136

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

	const editChatTitle = async (id, _title) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
137
		if (_title === '') {
Jannik Streidl's avatar
Jannik Streidl committed
138
			toast.error($i18n.t('Title cannot be an empty string.'));
Timothy J. Baek's avatar
Timothy J. Baek committed
139
140
141
142
143
144
145
146
		} else {
			title = _title;

			await updateChatById(localStorage.token, id, {
				title: _title
			});
			await chats.set(await getChatList(localStorage.token));
		}
147
148
149
	};

	const deleteChat = async (id) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
150
151
152
		const res = await deleteChatById(localStorage.token, id).catch((error) => {
			toast.error(error);
			chatDeleteId = null;
153

Timothy J. Baek's avatar
Timothy J. Baek committed
154
155
156
157
			return null;
		});

		if (res) {
Timothy J. Baek's avatar
Timothy J. Baek committed
158
159
160
161
			if ($chatId === id) {
				goto('/');
			}

Timothy J. Baek's avatar
Timothy J. Baek committed
162
163
			await chats.set(await getChatList(localStorage.token));
		}
164
	};
165
166
167
168
169
170

	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
171
172
173
174
175

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

Timothy J. Baek's avatar
Timothy J. Baek committed
178
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
Timothy J. Baek's avatar
Timothy J. Baek committed
179
180
181
182
183
184
<ArchivedChatsModal
	bind:show={showArchivedChatsModal}
	on:change={async () => {
		await chats.set(await getChatList(localStorage.token));
	}}
/>
Timothy J. Baek's avatar
Timothy J. Baek committed
185

Timothy J. Baek's avatar
Timothy J. Baek committed
186
187
188
189
<!-- svelte-ignore a11y-no-static-element-interactions -->

{#if $showSidebar}
	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
190
		class=" fixed md:hidden z-40 top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center overflow-hidden overscroll-contain"
Timothy J. Baek's avatar
Timothy J. Baek committed
191
192
193
194
195
196
		on:mousedown={() => {
			showSidebar.set(!$showSidebar);
		}}
	/>
{/if}

197
198
<div
	bind:this={navElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
199
200
	id="sidebar"
	class="h-screen max-h-[100dvh] min-h-screen {$showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
201
		? 'lg:relative w-[260px]'
Timothy J. Baek's avatar
Timothy J. Baek committed
202
		: '-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 rounded-r-2xl
203
        "
Timothy J. Baek's avatar
Timothy J. Baek committed
204
	data-state={$showSidebar}
205
>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
206
	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
207
		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
208
209
210
			? ''
			: 'invisible'}"
	>
Timothy J. Baek's avatar
Timothy J. Baek committed
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
		<div class="px-2 flex justify-between space-x-2">
			<button
				class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
				on:click={() => {
					showSidebar.set(!$showSidebar);
				}}
			>
				<div class=" m-auto self-center">
					<svg
						xmlns="http://www.w3.org/2000/svg"
						fill="none"
						viewBox="0 0 24 24"
						stroke-width="2"
						stroke="currentColor"
						class="size-5"
					>
						<path
							stroke-linecap="round"
							stroke-linejoin="round"
							d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
						/>
					</svg>
				</div>
			</button>
235
			<a
Timothy J. Baek's avatar
Timothy J. Baek committed
236
				id="sidebar-new-chat-button"
237
				class="flex justify-between rounded-xl px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
238
				href="/"
Timothy J. Baek's avatar
Timothy J. Baek committed
239
				on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
240
241
					selectedChatId = null;

242
					await goto('/');
243
					const newChatButton = document.getElementById('new-chat-button');
244
245
246
					setTimeout(() => {
						newChatButton?.click();
					}, 0);
247
248
249
250
251
252
253
				}}
			>
				<div class="self-center">
					<svg
						xmlns="http://www.w3.org/2000/svg"
						viewBox="0 0 20 20"
						fill="currentColor"
Timothy J. Baek's avatar
Timothy J. Baek committed
254
						class="size-5"
255
256
257
258
259
260
261
262
263
					>
						<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>
264
			</a>
265
266
		</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
267
		{#if $user?.role === 'admin'}
Timothy J. Baek's avatar
Timothy J. Baek committed
268
			<div class="px-2 flex justify-center mt-0.5">
269
				<a
Timothy J. Baek's avatar
Timothy J. Baek committed
270
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
271
					href="/modelfiles"
Timothy J. Baek's avatar
Timothy J. Baek committed
272
273
274
275
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
276
277
278
279
280
281
				>
					<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
282
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
283
284
285
286
287
288
							stroke="currentColor"
							class="w-4 h-4"
						>
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
Timothy J. Baek's avatar
Timothy J. Baek committed
289
								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
290
291
292
							/>
						</svg>
					</div>
293

Timothy J. Baek's avatar
Timothy J. Baek committed
294
					<div class="flex self-center">
295
						<div class=" self-center font-medium text-sm">{$i18n.t('Modelfiles')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
296
					</div>
297
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
298
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
299

Timothy J. Baek's avatar
Timothy J. Baek committed
300
			<div class="px-2 flex justify-center">
301
				<a
Timothy J. Baek's avatar
Timothy J. Baek committed
302
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
303
					href="/prompts"
Timothy J. Baek's avatar
Timothy J. Baek committed
304
305
306
307
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
308
309
310
311
				>
					<div class="self-center">
						<svg
							xmlns="http://www.w3.org/2000/svg"
Timothy J. Baek's avatar
Timothy J. Baek committed
312
313
							fill="none"
							viewBox="0 0 24 24"
Timothy J. Baek's avatar
Timothy J. Baek committed
314
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
315
							stroke="currentColor"
Timothy J. Baek's avatar
Timothy J. Baek committed
316
317
318
							class="w-4 h-4"
						>
							<path
Timothy J. Baek's avatar
Timothy J. Baek committed
319
320
321
								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
322
323
324
325
326
							/>
						</svg>
					</div>

					<div class="flex self-center">
327
						<div class=" self-center font-medium text-sm">{$i18n.t('Prompts')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
328
					</div>
329
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
330
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
331

Timothy J. Baek's avatar
Timothy J. Baek committed
332
			<div class="px-2 flex justify-center mb-1">
333
				<a
Timothy J. Baek's avatar
Timothy J. Baek committed
334
					class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
335
					href="/documents"
Timothy J. Baek's avatar
Timothy J. Baek committed
336
337
338
339
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
340
341
342
343
344
345
				>
					<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
346
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
347
348
349
350
351
352
353
354
355
356
357
358
							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">
359
						<div class=" self-center font-medium text-sm">{$i18n.t('Documents')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
360
					</div>
361
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
362
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
363
		{/if}
364

365
366
		<div class="relative flex flex-col flex-1 overflow-y-auto">
			{#if !($settings.saveChatHistory ?? true)}
Timothy J. Baek's avatar
Timothy J. Baek committed
367
				<div class="absolute z-40 w-full h-full bg-gray-50/90 dark:bg-black/90 flex justify-center">
368
					<div class=" text-left px-5 py-2">
369
						<div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
370
						<div class="text-xs mt-2">
Jannik Streidl's avatar
Jannik Streidl committed
371
372
373
374
375
							{$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
376
377
378
379
380
							>
						</div>

						<div class="mt-3">
							<button
Timothy J. Baek's avatar
Timothy J. Baek committed
381
								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"
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
								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
402
								<div>{$i18n.t('Enable Chat History')}</div>
403
404
405
							</button>
						</div>
					</div>
406
				</div>
407
			{/if}
408

Timothy J. Baek's avatar
Timothy J. Baek committed
409
			<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
410
				<div class="flex w-full" id="chat-search">
411
					<div class="self-center pl-3 py-2 rounded-l-xl bg-white dark:bg-gray-950">
412
413
414
415
416
417
418
419
420
421
422
423
424
						<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>
425

426
					<input
427
						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
428
						placeholder={$i18n.t('Search')}
429
						bind:value={search}
Timothy J. Baek's avatar
Timothy J. Baek committed
430
431
432
						on:focus={() => {
							enrichChatsWithContent($chats);
						}}
433
434
435
					/>
				</div>
			</div>
436

Timothy J. Baek's avatar
Timothy J. Baek committed
437
438
439
			{#if $tags.length > 0}
				<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
					<button
440
						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
441
442
443
444
						on:click={async () => {
							await chats.set(await getChatList(localStorage.token));
						}}
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
445
						{$i18n.t('all')}
Timothy J. Baek's avatar
Timothy J. Baek committed
446
447
448
					</button>
					{#each $tags as tag}
						<button
449
							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
450
							on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
451
452
453
454
455
456
								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
457
458
459
460
461
462
463
464
							}}
						>
							{tag.name}
						</button>
					{/each}
				</div>
			{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
465
			<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-none">
466
				{#each filteredChatList as chat, idx}
Timothy J. Baek's avatar
Timothy J. Baek committed
467
					{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
468
469
470
						<div
							class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
								? ''
Timothy J. Baek's avatar
Timothy J. Baek committed
471
								: 'pt-5'} pb-0.5"
472
						>
Timothy J. Baek's avatar
Timothy J. Baek committed
473
							{$i18n.t(chat.time_range)}
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
							<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
							{$i18n.t('Today')}
							{$i18n.t('Yesterday')}
							{$i18n.t('Previous 7 days')}
							{$i18n.t('Previous 30 days')}
							{$i18n.t('January')}
							{$i18n.t('February')}
							{$i18n.t('March')}
							{$i18n.t('April')}
							{$i18n.t('May')}
							{$i18n.t('June')}
							{$i18n.t('July')}
							{$i18n.t('August')}
							{$i18n.t('September')}
							{$i18n.t('October')}
							{$i18n.t('November')}
							{$i18n.t('December')}
							-->
492
493
494
						</div>
					{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
495
					<div class=" w-full pr-2 relative group">
Timothy J. Baek's avatar
Timothy J. Baek committed
496
497
						{#if chatTitleEditId === chat.id}
							<div
Timothy J. Baek's avatar
Timothy J. Baek committed
498
499
500
								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
501
									? 'bg-gray-200 dark:bg-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
502
									: chat.id === selectedChatId
503
504
									? '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
505
506
507
508
509
							>
								<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
510
511
512
								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
513
									? 'bg-gray-200 dark:bg-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
514
									: chat.id === selectedChatId
515
516
									? '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
517
								href="/c/{chat.id}"
518
								on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
519
									selectedChatId = chat.id;
520
									if (window.innerWidth < 1024) {
Timothy J. Baek's avatar
Timothy J. Baek committed
521
										showSidebar.set(false);
522
523
									}
								}}
Timothy J. Baek's avatar
Timothy J. Baek committed
524
								draggable="false"
Timothy J. Baek's avatar
Timothy J. Baek committed
525
526
							>
								<div class=" flex self-center flex-1 w-full">
Timothy J. Baek's avatar
Timothy J. Baek committed
527
									<div class=" text-left self-center overflow-hidden w-full h-[20px]">
528
										{chat.title}
Timothy J. Baek's avatar
Timothy J. Baek committed
529
									</div>
530
								</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
531
532
							</a>
						{/if}
533

Timothy J. Baek's avatar
Timothy J. Baek committed
534
						<div
Timothy J. Baek's avatar
Timothy J. Baek committed
535
							class="
536

Timothy J. Baek's avatar
Timothy J. Baek committed
537
							{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
Timothy J. Baek's avatar
Timothy J. Baek committed
538
								? 'from-gray-200 dark:from-gray-900'
Timothy J. Baek's avatar
Timothy J. Baek committed
539
								: chat.id === selectedChatId
540
541
								? '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
542
								absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
543

Timothy J. Baek's avatar
Timothy J. Baek committed
544
								  to-transparent"
Timothy J. Baek's avatar
Timothy J. Baek committed
545
546
547
548
						>
							{#if chatTitleEditId === chat.id}
								<div class="flex self-center space-x-1.5 z-10">
									<button
549
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
550
551
552
553
554
555
556
557
558
559
560
										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"
561
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
562
563
564
565
566
567
568
569
											<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
570
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
571
572
573
574
575
576
577
578
579
580
										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"
581
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
582
583
584
585
586
587
588
589
590
											<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
591
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
592
593
594
595
596
597
598
599
600
										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"
601
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
602
603
604
605
606
607
608
609
											<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
610
										class=" self-center dark:hover:text-white transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
611
612
613
614
615
616
617
618
619
										on:click={() => {
											chatDeleteId = null;
										}}
									>
										<svg
											xmlns="http://www.w3.org/2000/svg"
											viewBox="0 0 20 20"
											fill="currentColor"
											class="w-4 h-4"
620
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
621
622
623
624
625
626
627
											<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
628
								<div class="flex self-center space-x-1 z-10">
Timothy J. Baek's avatar
Timothy J. Baek committed
629
									<ChatMenu
Timothy J. Baek's avatar
Timothy J. Baek committed
630
										chatId={chat.id}
Timothy J. Baek's avatar
Timothy J. Baek committed
631
632
633
634
										shareHandler={() => {
											shareChatId = selectedChatId;
											showShareChatModal = true;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
635
										renameHandler={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
636
637
638
											chatTitle = chat.title;
											chatTitleEditId = chat.id;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
639
										deleteHandler={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
640
641
											chatDeleteId = chat.id;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
642
643
644
										onClose={() => {
											selectedChatId = null;
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
645
									>
646
										<button
Timothy J. Baek's avatar
Timothy J. Baek committed
647
											aria-label="Chat Menu"
648
											class=" self-center dark:hover:text-white transition"
649
											on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
650
												selectedChatId = chat.id;
651
											}}
652
										>
653
654
											<svg
												xmlns="http://www.w3.org/2000/svg"
Timothy J. Baek's avatar
Timothy J. Baek committed
655
656
												viewBox="0 0 16 16"
												fill="currentColor"
657
658
659
												class="w-4 h-4"
											>
												<path
Timothy J. Baek's avatar
Timothy J. Baek committed
660
													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"
661
662
663
												/>
											</svg>
										</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
664
									</ChatMenu>
Timothy J. Baek's avatar
Timothy J. Baek committed
665

666
									<Tooltip content={$i18n.t('Archive')}>
Timothy J. Baek's avatar
Timothy J. Baek committed
667
668
669
670
										<button
											aria-label="Archive"
											class=" self-center dark:hover:text-white transition"
											on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
671
												archiveChatHandler(chat.id);
Timothy J. Baek's avatar
Timothy J. Baek committed
672
673
674
675
676
											}}
										>
											<ArchiveBox />
										</button>
									</Tooltip>
Timothy J. Baek's avatar
Timothy J. Baek committed
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697

									{#if chat.id === $chatId}
										<button
											id="delete-chat-button"
											class="hidden"
											on:click={() => {
												chatDeleteId = chat.id;
											}}
										>
											<svg
												xmlns="http://www.w3.org/2000/svg"
												viewBox="0 0 16 16"
												fill="currentColor"
												class="w-4 h-4"
											>
												<path
													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"
												/>
											</svg>
										</button>
									{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
698
699
700
								</div>
							{/if}
						</div>
701
702
703
					</div>
				{/each}
			</div>
704
705
706
		</div>

		<div class="px-2.5">
Timothy J. Baek's avatar
Timothy J. Baek committed
707
			<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
708
709
710

			<div class="flex flex-col">
				{#if $user !== undefined}
Timothy J. Baek's avatar
Timothy J. Baek committed
711
712
713
714
715
716
					<UserMenu
						role={$user.role}
						on:show={(e) => {
							if (e.detail === 'archived-chat') {
								showArchivedChatsModal = true;
							}
717
718
						}}
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
719
720
721
722
723
						<button
							class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
							on:click={() => {
								showDropdown = !showDropdown;
							}}
724
						>
Timothy J. Baek's avatar
Timothy J. Baek committed
725
726
727
728
729
730
							<div class=" self-center mr-3">
								<img
									src={$user.profile_image_url}
									class=" max-w-[30px] object-cover rounded-full"
									alt="User profile"
								/>
731
							</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
732
733
734
							<div class=" self-center font-semibold">{$user.name}</div>
						</button>
					</UserMenu>
735
736
737
738
739
740
				{/if}
			</div>
		</div>
	</div>

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

<style>
Timothy J. Baek's avatar
Timothy J. Baek committed
779
780
781
782
	.scrollbar-none:active::-webkit-scrollbar-thumb,
	.scrollbar-none:focus::-webkit-scrollbar-thumb,
	.scrollbar-none:hover::-webkit-scrollbar-thumb {
		visibility: visible;
Timothy J. Baek's avatar
Timothy J. Baek committed
783
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
784
785
	.scrollbar-none::-webkit-scrollbar-thumb {
		visibility: hidden;
Timothy J. Baek's avatar
Timothy J. Baek committed
786
787
	}
</style>