Sidebar.svelte 18 KB
Newer Older
1
<script lang="ts">
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
2
	import { toast } from 'svelte-sonner';
Timothy J. Baek's avatar
Timothy J. Baek committed
3
	import { goto } from '$app/navigation';
Timothy J. Baek's avatar
Timothy J. Baek committed
4
5
6
7
8
9
10
11
	import {
		user,
		chats,
		settings,
		showSettings,
		chatId,
		tags,
		showSidebar,
Timothy J. Baek's avatar
Timothy J. Baek committed
12
		mobile,
Timothy J. Baek's avatar
Timothy J. Baek committed
13
		showArchivedChats,
14
15
16
17
		pinnedChats,
		pageSkip,
		pageLimit,
		scrollPaginationEnabled
Timothy J. Baek's avatar
Timothy J. Baek committed
18
	} from '$lib/stores';
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
19
	import { onMount, getContext, tick } from 'svelte';
20
21
22

	const i18n = getContext('i18n');

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
23
	import { updateUserSettings } from '$lib/apis/users';
Timothy J. Baek's avatar
Timothy J. Baek committed
24
25
26
	import {
		deleteChatById,
		getChatList,
Henry Holloway's avatar
Henry Holloway committed
27
		getChatById,
Timothy J. Baek's avatar
Timothy J. Baek committed
28
		getChatListByTagName,
Timothy J. Baek's avatar
Timothy J. Baek committed
29
		updateChatById,
Timothy J. Baek's avatar
Timothy J. Baek committed
30
		getAllChatTags,
Timothy J. Baek's avatar
Timothy J. Baek committed
31
32
		archiveChatById,
		cloneChatById
Timothy J. Baek's avatar
Timothy J. Baek committed
33
	} from '$lib/apis/chats';
34
	import { WEBUI_BASE_URL } from '$lib/constants';
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
35

36
	import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
37
	import UserMenu from './Sidebar/UserMenu.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
38
	import ChatItem from './Sidebar/ChatItem.svelte';
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
39
	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
40

Timothy J. Baek's avatar
Timothy J. Baek committed
41
	const BREAKPOINT = 768;
Timothy J. Baek's avatar
Timothy J. Baek committed
42

43
	let navElement;
44
	let search = '';
45

46
47
	let shiftKey = false;

Timothy J. Baek's avatar
Timothy J. Baek committed
48
	let selectedChatId = null;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
49
	let deleteChat = null;
50

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
51
	let showDeleteConfirm = false;
52
	let showDropdown = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
53

54
	let filteredChatList = [];
55
56
57
58
59
60
	let paginationScrollThreashold = 0.6;
	let nextPageLoading = false;
	let tagView = false;
	let chatPagniationComplete = false;

	pageLimit.set(20);
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

	$: 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;
		}
	});

82
	onMount(async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
83
84
85
86
87
88
89
90
91
92
		mobile.subscribe((e) => {
			if ($showSidebar && e) {
				showSidebar.set(false);
			}

			if (!$showSidebar && !e) {
				showSidebar.set(true);
			}
		});

Timothy J. Baek's avatar
Timothy J. Baek committed
93
		showSidebar.set(window.innerWidth > BREAKPOINT);
Timothy J. Baek's avatar
Timothy J. Baek committed
94
95

		await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
96
		await chats.set(await getChatList(localStorage.token, $pageSkip, $pageLimit));
Timothy J. Baek's avatar
Timothy J. Baek committed
97

98
99
		let touchstart;
		let touchend;
Timothy J. Baek's avatar
Timothy J. Baek committed
100
101
102

		function checkDirection() {
			const screenWidth = window.innerWidth;
103
			const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
Timothy J. Baek's avatar
Timothy J. Baek committed
104
			if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
105
				if (touchend.screenX < touchstart.screenX) {
Timothy J. Baek's avatar
Timothy J. Baek committed
106
					showSidebar.set(false);
Timothy J. Baek's avatar
Timothy J. Baek committed
107
				}
108
				if (touchend.screenX > touchstart.screenX) {
Timothy J. Baek's avatar
Timothy J. Baek committed
109
					showSidebar.set(true);
Timothy J. Baek's avatar
Timothy J. Baek committed
110
111
112
113
				}
			}
		}

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
114
		const onTouchStart = (e) => {
115
116
			touchstart = e.changedTouches[0];
			console.log(touchstart.clientX);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
117
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
118

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
119
		const onTouchEnd = (e) => {
120
			touchend = e.changedTouches[0];
Timothy J. Baek's avatar
Timothy J. Baek committed
121
			checkDirection();
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
122
123
		};

124
125
126
127
128
129
130
131
132
133
134
135
		const onKeyDown = (e) => {
			if (e.key === 'Shift') {
				shiftKey = true;
			}
		};

		const onKeyUp = (e) => {
			if (e.key === 'Shift') {
				shiftKey = false;
			}
		};

Timothy J. Baek's avatar
Timothy J. Baek committed
136
137
138
139
		const onFocus = () => {};

		const onBlur = () => {
			shiftKey = false;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
140
			selectedChatId = null;
Timothy J. Baek's avatar
Timothy J. Baek committed
141
142
143
144
		};

		window.addEventListener('keydown', onKeyDown);
		window.addEventListener('keyup', onKeyUp);
145

146
147
		window.addEventListener('touchstart', onTouchStart);
		window.addEventListener('touchend', onTouchEnd);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
148

Timothy J. Baek's avatar
Timothy J. Baek committed
149
150
151
		window.addEventListener('focus', onFocus);
		window.addEventListener('blur', onBlur);

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
152
		return () => {
153
154
155
			window.removeEventListener('keydown', onKeyDown);
			window.removeEventListener('keyup', onKeyUp);

156
157
			window.removeEventListener('touchstart', onTouchStart);
			window.removeEventListener('touchend', onTouchEnd);
Timothy J. Baek's avatar
Timothy J. Baek committed
158
159
160

			window.removeEventListener('focus', onFocus);
			window.removeEventListener('blur', onBlur);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
161
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
	});

	// 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);
	};
178

179
180
	const saveSettings = async (updated) => {
		await settings.set({ ...$settings, ...updated });
181
		await updateUserSettings(localStorage.token, { ui: $settings });
182
183
		location.href = '/';
	};
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
184
185
186
187
188
189
190
191
192
193
194
195
196

	const deleteChatHandler = async (id) => {
		const res = await deleteChatById(localStorage.token, id).catch((error) => {
			toast.error(error);
			return null;
		});

		if (res) {
			if ($chatId === id) {
				await chatId.set('');
				await tick();
				goto('/');
			}
197
198
199
			await chats.set(
				await getChatList(localStorage.token, 0, $pageSkip * $pageLimit || $pageLimit)
			);
200
			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
201
202
		}
	};
203
204
</script>

Timothy J. Baek's avatar
Timothy J. Baek committed
205
<ArchivedChatsModal
Timothy J. Baek's avatar
Timothy J. Baek committed
206
	bind:show={$showArchivedChats}
Timothy J. Baek's avatar
Timothy J. Baek committed
207
208
209
210
	on:change={async () => {
		await chats.set(await getChatList(localStorage.token));
	}}
/>
Timothy J. Baek's avatar
Timothy J. Baek committed
211

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
212
213
<DeleteConfirmDialog
	bind:show={showDeleteConfirm}
214
	title={$i18n.t('Delete chat?')}
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
215
216
217
218
219
	on:confirm={() => {
		deleteChatHandler(deleteChat.id);
	}}
>
	<div class=" text-sm text-gray-500">
220
		{$i18n.t('This will delete')} <span class="  font-semibold">{deleteChat.title}</span>.
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
221
222
223
	</div>
</DeleteConfirmDialog>

Timothy J. Baek's avatar
Timothy J. Baek committed
224
225
226
227
<!-- svelte-ignore a11y-no-static-element-interactions -->

{#if $showSidebar}
	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
228
		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
229
230
231
232
233
234
		on:mousedown={() => {
			showSidebar.set(!$showSidebar);
		}}
	/>
{/if}

235
236
<div
	bind:this={navElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
237
	id="sidebar"
Timothy J. Baek's avatar
Timothy J. Baek committed
238
	class="h-screen max-h-[100dvh] min-h-screen select-none {$showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
239
		? 'md:relative w-[260px]'
Timothy J. Baek's avatar
Timothy J. Baek committed
240
		: '-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
241
        "
Timothy J. Baek's avatar
Timothy J. Baek committed
242
	data-state={$showSidebar}
243
>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
244
	<div
Timothy J. Baek's avatar
Timothy J. Baek committed
245
		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
246
247
248
			? ''
			: 'invisible'}"
	>
249
250
251
		<h1 class="text-red-400 text-bold text-xl">
			Chats loaded: {$chats.length}
		</h1>
Timothy J. Baek's avatar
Timothy J. Baek committed
252
		<div class="px-2.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400">
253
			<a
Timothy J. Baek's avatar
Timothy J. Baek committed
254
				id="sidebar-new-chat-button"
Timothy J. Baek's avatar
Timothy J. Baek committed
255
				class="flex flex-1 justify-between rounded-xl px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
256
				href="/"
257
				draggable="false"
Timothy J. Baek's avatar
Timothy J. Baek committed
258
				on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
259
					selectedChatId = null;
260
					await goto('/');
261
					const newChatButton = document.getElementById('new-chat-button');
262
263
					setTimeout(() => {
						newChatButton?.click();
Timothy J. Baek's avatar
Timothy J. Baek committed
264
265
266
						if ($mobile) {
							showSidebar.set(false);
						}
267
					}, 0);
268
269
				}}
			>
Timothy J. Baek's avatar
Timothy J. Baek committed
270
271
				<div class="self-center mx-1.5">
					<img
Timothy J. Baek's avatar
Timothy J. Baek committed
272
						crossorigin="anonymous"
Timothy J. Baek's avatar
Timothy J. Baek committed
273
274
275
276
277
						src="{WEBUI_BASE_URL}/static/favicon.png"
						class=" size-6 -translate-x-1.5 rounded-full"
						alt="logo"
					/>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
278
				<div class=" self-center font-medium text-sm text-gray-850 dark:text-white font-primary">
Timothy J. Baek's avatar
Timothy J. Baek committed
279
280
					{$i18n.t('New Chat')}
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
281
				<div class="self-center ml-auto">
282
283
284
285
					<svg
						xmlns="http://www.w3.org/2000/svg"
						viewBox="0 0 20 20"
						fill="currentColor"
Timothy J. Baek's avatar
Timothy J. Baek committed
286
						class="size-5"
287
288
289
290
291
292
293
294
295
					>
						<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>
296
			</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
297
298

			<button
Timothy J. Baek's avatar
Timothy J. Baek committed
299
				class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-900 transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
				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>
321
322
		</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
323
		{#if $user?.role === 'admin'}
Timothy J. Baek's avatar
Timothy J. Baek committed
324
			<div class="px-2.5 flex justify-center text-gray-800 dark:text-gray-200">
325
				<a
Timothy J. Baek's avatar
Timothy J. Baek committed
326
					class="flex-grow flex space-x-3 rounded-xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
Timothy J. Baek's avatar
Timothy J. Baek committed
327
					href="/workspace"
Timothy J. Baek's avatar
Timothy J. Baek committed
328
329
330
					on:click={() => {
						selectedChatId = null;
						chatId.set('');
Timothy J. Baek's avatar
Timothy J. Baek committed
331
332
333
334

						if ($mobile) {
							showSidebar.set(false);
						}
Timothy J. Baek's avatar
Timothy J. Baek committed
335
					}}
336
					draggable="false"
Timothy J. Baek's avatar
Timothy J. Baek committed
337
338
339
340
341
342
				>
					<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
343
							stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
344
							stroke="currentColor"
Timothy J. Baek's avatar
Timothy J. Baek committed
345
							class="size-[1.1rem]"
Timothy J. Baek's avatar
Timothy J. Baek committed
346
347
348
349
						>
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
Timothy J. Baek's avatar
Timothy J. Baek committed
350
								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
351
352
353
							/>
						</svg>
					</div>
354

Timothy J. Baek's avatar
Timothy J. Baek committed
355
					<div class="flex self-center">
Timothy J. Baek's avatar
Timothy J. Baek committed
356
						<div class=" self-center font-medium text-sm font-primary">{$i18n.t('Workspace')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
357
					</div>
358
				</a>
Timothy J. Baek's avatar
Timothy J. Baek committed
359
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
360
		{/if}
361

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

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

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

423
					<input
Timothy J. Baek's avatar
Timothy J. Baek committed
424
						class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none"
Jannik Streidl's avatar
Jannik Streidl committed
425
						placeholder={$i18n.t('Search')}
426
						bind:value={search}
427
428
429
430
431
432
433
434
435
						on:focus={async () => {
							// loading all chats. disable pagination on scrol.
							scrollPaginationEnabled.set(false);
							// subsequent queries will calculate page size to rehydrate the ui.
							// since every chat is already loaded, the calculation should now load all chats.
							pageSkip.set(0);
							pageLimit.set(-1);
							await chats.set(await getChatList(localStorage.token)); // when searching, load all chats

Timothy J. Baek's avatar
Timothy J. Baek committed
436
437
							enrichChatsWithContent($chats);
						}}
438
439
440
					/>
				</div>
			</div>
441

Timothy J. Baek's avatar
Timothy J. Baek committed
442
			{#if $tags.filter((t) => t.name !== 'pinned').length > 0}
Timothy J. Baek's avatar
Timothy J. Baek committed
443
				<div class="px-2.5 mb-2 flex gap-1 flex-wrap">
Timothy J. Baek's avatar
Timothy J. Baek committed
444
					<button
Timothy J. Baek's avatar
Timothy J. Baek committed
445
						class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
Timothy J. Baek's avatar
Timothy J. Baek committed
446
						on:click={async () => {
447
448
449
450
451
452
453
							scrollPaginationEnabled.set(false);
							pageSkip.set(0);
							pageLimit.set(-1);

							await chats.set(
								await getChatList(localStorage.token, $pageSkip * $pageLimit, $pageLimit)
							);
Timothy J. Baek's avatar
Timothy J. Baek committed
454
455
						}}
					>
Timothy J. Baek's avatar
Timothy J. Baek committed
456
						{$i18n.t('all')}
Timothy J. Baek's avatar
Timothy J. Baek committed
457
					</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
458
					{#each $tags.filter((t) => t.name !== 'pinned') as tag}
Timothy J. Baek's avatar
Timothy J. Baek committed
459
						<button
Timothy J. Baek's avatar
Timothy J. Baek committed
460
							class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
Timothy J. Baek's avatar
Timothy J. Baek committed
461
							on:click={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
462
463
								let chatIds = await getChatListByTagName(localStorage.token, tag.name);
								if (chatIds.length === 0) {
464
									// no chats found in the tag
Timothy J. Baek's avatar
Timothy J. Baek committed
465
									await tags.set(await getAllChatTags(localStorage.token));
466
467
468
469
470
471
472
473
									scrollPaginationEnabled.set(false);
									pageSkip.set(0);
									pageLimit.set(-1);
									chatIds = await getChatList(
										localStorage.token,
										$pageSkip * $pageLimit,
										$pageLimit
									);
Timothy J. Baek's avatar
Timothy J. Baek committed
474
								}
475
								tagView = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
476
								await chats.set(chatIds);
Timothy J. Baek's avatar
Timothy J. Baek committed
477
478
479
480
481
482
483
484
							}}
						>
							{tag.name}
						</button>
					{/each}
				</div>
			{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
			{#if $pinnedChats.length > 0}
				<div class="pl-2 py-2 flex flex-col space-y-1">
					<div class="">
						<div class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium pb-1.5">
							{$i18n.t('Pinned')}
						</div>

						{#each $pinnedChats as chat, idx}
							<ChatItem
								{chat}
								{shiftKey}
								selected={selectedChatId === chat.id}
								on:select={() => {
									selectedChatId = chat.id;
								}}
								on:unselect={() => {
									selectedChatId = null;
								}}
								on:delete={(e) => {
									if ((e?.detail ?? '') === 'shift') {
										deleteChatHandler(chat.id);
									} else {
										deleteChat = chat;
										showDeleteConfirm = true;
									}
								}}
							/>
						{/each}
					</div>
				</div>
			{/if}

517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
			<div
				class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden"
				on:scroll={async (e) => {
					if (!$scrollPaginationEnabled) return;
					if (tagView) return;
					if (nextPageLoading) return;
					if (chatPagniationComplete) return;

					const maxScroll = e.target.scrollHeight - e.target.clientHeight;
					const currentPos = e.target.scrollTop;
					const ratio = currentPos / maxScroll;
					if (ratio >= paginationScrollThreashold) {
						nextPageLoading = true;
						pageSkip.set($pageSkip + 1);
						// extend existing chats
						const nextPageChats = await getChatList(
							localStorage.token,
							$pageSkip * $pageLimit,
							$pageLimit
						);
						// once the bottom of the list has been reached (no results) there is no need to continue querying
						chatPagniationComplete = nextPageChats.length === 0;
						await chats.set([...$chats, ...nextPageChats]);
						nextPageLoading = false;
					}
				}}
			>
544
				{#each filteredChatList as chat, idx}
Timothy J. Baek's avatar
Timothy J. Baek committed
545
					{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
546
547
548
						<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
549
								: 'pt-5'} pb-0.5"
550
						>
Timothy J. Baek's avatar
Timothy J. Baek committed
551
							{$i18n.t(chat.time_range)}
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
							<!-- 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')}
							-->
570
571
572
						</div>
					{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
573
574
					<ChatItem
						{chat}
575
						{shiftKey}
Timothy J. Baek's avatar
Timothy J. Baek committed
576
577
578
579
						selected={selectedChatId === chat.id}
						on:select={() => {
							selectedChatId = chat.id;
						}}
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
580
581
582
						on:unselect={() => {
							selectedChatId = null;
						}}
Timothy J. Baek's avatar
Timothy J. Baek committed
583
584
585
586
587
588
589
						on:delete={(e) => {
							if ((e?.detail ?? '') === 'shift') {
								deleteChatHandler(chat.id);
							} else {
								deleteChat = chat;
								showDeleteConfirm = true;
							}
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
590
						}}
Timothy J. Baek's avatar
Timothy J. Baek committed
591
					/>
592
593
				{/each}
			</div>
594
595
		</div>

596
597
598
		<div class="px-2.5">
			<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->

Timothy J. Baek's avatar
Timothy J. Baek committed
599
			<div class="flex flex-col font-primary">
600
601
602
603
604
605
606
607
608
609
610
611
612
				{#if $user !== undefined}
					<UserMenu
						role={$user.role}
						on:show={(e) => {
							if (e.detail === 'archived-chat') {
								showArchivedChats.set(true);
							}
						}}
					>
						<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;
Timothy J. Baek's avatar
Timothy J. Baek committed
613
							}}
614
						>
615
616
617
618
619
620
621
							<div class=" self-center mr-3">
								<img
									src={$user.profile_image_url}
									class=" max-w-[30px] object-cover rounded-full"
									alt="User profile"
								/>
							</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
622
							<div class=" self-center font-medium">{$user.name}</div>
623
624
625
						</button>
					</UserMenu>
				{/if}
626
			</div>
627
		</div>
628
629
	</div>
</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
630
631

<style>
Timothy J. Baek's avatar
Timothy J. Baek committed
632
633
634
	.scrollbar-hidden:active::-webkit-scrollbar-thumb,
	.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
	.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
Timothy J. Baek's avatar
Timothy J. Baek committed
635
		visibility: visible;
Timothy J. Baek's avatar
Timothy J. Baek committed
636
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
637
	.scrollbar-hidden::-webkit-scrollbar-thumb {
Timothy J. Baek's avatar
Timothy J. Baek committed
638
		visibility: hidden;
Timothy J. Baek's avatar
Timothy J. Baek committed
639
640
	}
</style>