Browse Source

enh: drag and drop chat to pin

Timothy J. Baek 6 months ago
parent
commit
2f4c04055c

+ 4 - 3
src/lib/components/common/DragGhost.svelte

@@ -22,8 +22,9 @@
 
 <div
 	bind:this={popupElement}
-	class=" absolute text-white z-[99999]"
-	style="top: {y}px; left: {x}px;"
+	class="fixed top-0 left-0 w-screen h-[100dvh] z-50 touch-none pointer-events-none"
 >
-	<slot></slot>
+	<div class=" absolute text-white z-[99999]" style="top: {y}px; left: {x}px;">
+		<slot></slot>
+	</div>
 </div>

+ 85 - 39
src/lib/components/common/Folder.svelte

@@ -1,5 +1,5 @@
 <script>
-	import { getContext, createEventDispatcher } from 'svelte';
+	import { getContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
 
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
@@ -9,48 +9,94 @@
 	import Collapsible from './Collapsible.svelte';
 
 	export let open = true;
+
+	export let id = '';
 	export let name = '';
+	export let collapsible = true;
+
+	let folderElement;
+
+	let dragged = false;
+
+	const onDragOver = (e) => {
+		e.preventDefault();
+		dragged = true;
+	};
+
+	const onDrop = (e) => {
+		e.preventDefault();
+
+		if (folderElement.contains(e.target)) {
+			console.log('Dropped on the Button');
+
+			// get data from the drag event
+			const dataTransfer = e.dataTransfer.getData('text/plain');
+			const data = JSON.parse(dataTransfer);
+			console.log(data);
+			dispatch('drop', data);
+
+			dragged = false;
+		}
+	};
+
+	const onDragLeave = (e) => {
+		e.preventDefault();
+		dragged = false;
+	};
+
+	onMount(() => {
+		folderElement.addEventListener('dragover', onDragOver);
+		folderElement.addEventListener('drop', onDrop);
+		folderElement.addEventListener('dragleave', onDragLeave);
+	});
+
+	onDestroy(() => {
+		folderElement.addEventListener('dragover', onDragOver);
+		folderElement.removeEventListener('drop', onDrop);
+		folderElement.removeEventListener('dragleave', onDragLeave);
+	});
 </script>
 
-<div>
-	<Collapsible
-		bind:open
-		className="w-full"
-		buttonClassName="w-full"
-		on:change={(e) => {
-			dispatch('change', e.detail);
-		}}
-	>
-		<!-- svelte-ignore a11y-no-static-element-interactions -->
+<div bind:this={folderElement} class="relative">
+	{#if dragged}
 		<div
-			class="mx-2 w-full"
-			on:dragenter={(e) => {
-				e.stopPropagation();
-			}}
-			on:drop={(e) => {
-				console.log('Dropped on the Button');
+			class="absolute top-0 left-0 w-full h-full rounded-sm bg-gray-200 bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
+		></div>
+	{/if}
+
+	{#if collapsible}
+		<Collapsible
+			bind:open
+			className="w-full "
+			buttonClassName="w-full"
+			on:change={(e) => {
+				dispatch('change', e.detail);
 			}}
-			on:dragleave={(e) => {}}
 		>
-			<button
-				class="w-full py-1 px-1.5 rounded-md flex items-center gap-1 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
-			>
-				<div class="text-gray-300">
-					{#if open}
-						<ChevronDown className=" size-3" strokeWidth="2.5" />
-					{:else}
-						<ChevronRight className=" size-3" strokeWidth="2.5" />
-					{/if}
-				</div>
-
-				<div class="translate-y-[0.5px]">
-					{name}
-				</div>
-			</button>
-		</div>
-
-		<div slot="content">
-			<slot></slot>
-		</div>
-	</Collapsible>
+			<!-- svelte-ignore a11y-no-static-element-interactions -->
+			<div class="mx-2 w-full">
+				<button
+					class="w-full py-1 px-1.5 rounded-md flex items-center gap-1 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+				>
+					<div class="text-gray-300">
+						{#if open}
+							<ChevronDown className=" size-3" strokeWidth="2.5" />
+						{:else}
+							<ChevronRight className=" size-3" strokeWidth="2.5" />
+						{/if}
+					</div>
+
+					<div class="translate-y-[0.5px]">
+						{name}
+					</div>
+				</button>
+			</div>
+
+			<div slot="content">
+				<slot></slot>
+			</div>
+		</Collapsible>
+	{:else}
+		<slot></slot>
+	{/if}
 </div>

+ 96 - 57
src/lib/components/layout/Sidebar.svelte

@@ -35,7 +35,9 @@
 		cloneChatById,
 		getChatListBySearchText,
 		createNewChat,
-		getPinnedChatList
+		getPinnedChatList,
+		toggleChatPinnedStatusById,
+		getChatPinnedStatusById
 	} from '$lib/apis/chats';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
@@ -363,8 +365,8 @@
 	bind:this={navElement}
 	id="sidebar"
 	class="h-screen max-h-[100dvh] min-h-screen select-none {$showSidebar
-		? 'md:relative w-[260px]'
-		: '-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
+		? 'md:relative w-[260px] max-w-[260px]'
+		: '-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 overflow-x-hidden
         "
 	data-state={$showSidebar}
 >
@@ -381,7 +383,7 @@
 		</div>
 	{/if}
 	<div
-		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar
+		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] overflow-x-hidden z-50 {$showSidebar
 			? ''
 			: 'invisible'}"
 	>
@@ -517,13 +519,27 @@
 			{/if}
 
 			{#if !search && $pinnedChats.length > 0}
-				<div class=" pb-2 flex flex-col space-y-1">
+				<div class=" flex flex-col space-y-1">
 					<Folder
 						bind:open={showPinnedChat}
 						on:change={(e) => {
 							localStorage.setItem('showPinnedChat', e.detail);
 							console.log(e.detail);
 						}}
+						on:drop={async (e) => {
+							const { id } = e.detail;
+
+							const status = await getChatPinnedStatusById(localStorage.token, id);
+
+							if (!status) {
+								const res = await toggleChatPinnedStatusById(localStorage.token, id);
+
+								if (res) {
+									await pinnedChats.set(await getPinnedChatList(localStorage.token));
+									initChatList();
+								}
+							}
+						}}
 						name={$i18n.t('Pinned')}
 					>
 						<div class="pl-2 mt-1 flex flex-col overflow-y-auto scrollbar-hidden">
@@ -557,17 +573,36 @@
 				</div>
 			{/if}
 
-			<div class="pl-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
-				{#if $chats}
-					{#each $chats as chat, idx}
-						{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
-							<div
-								class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
-									? ''
-									: 'pt-5'} pb-0.5"
-							>
-								{$i18n.t(chat.time_range)}
-								<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
+			<div class="flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
+				<Folder
+					collapsible={false}
+					on:drop={async (e) => {
+						const { id } = e.detail;
+
+						const status = await getChatPinnedStatusById(localStorage.token, id);
+
+						if (status) {
+							const res = await toggleChatPinnedStatusById(localStorage.token, id);
+
+							if (res) {
+								await pinnedChats.set(await getPinnedChatList(localStorage.token));
+								initChatList();
+							}
+						}
+					}}
+				>
+					<div class="pt-2 pl-2">
+						{#if $chats}
+							{#each $chats as chat, idx}
+								{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
+									<div
+										class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx ===
+										0
+											? ''
+											: 'pt-5'} pb-0.5"
+									>
+										{$i18n.t(chat.time_range)}
+										<!-- 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')}
@@ -585,54 +620,58 @@
 							{$i18n.t('November')}
 							{$i18n.t('December')}
 							-->
-							</div>
-						{/if}
+									</div>
+								{/if}
 
-						<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;
-								}
-							}}
-							on:tag={(e) => {
-								const { type, name } = e.detail;
-								tagEventHandler(type, name, chat.id);
-							}}
-						/>
-					{/each}
+								<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;
+										}
+									}}
+									on:tag={(e) => {
+										const { type, name } = e.detail;
+										tagEventHandler(type, name, chat.id);
+									}}
+								/>
+							{/each}
 
-					{#if $scrollPaginationEnabled && !allChatsLoaded}
-						<Loader
-							on:visible={(e) => {
-								if (!chatListLoading) {
-									loadMoreChats();
-								}
-							}}
-						>
+							{#if $scrollPaginationEnabled && !allChatsLoaded}
+								<Loader
+									on:visible={(e) => {
+										if (!chatListLoading) {
+											loadMoreChats();
+										}
+									}}
+								>
+									<div
+										class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"
+									>
+										<Spinner className=" size-4" />
+										<div class=" ">Loading...</div>
+									</div>
+								</Loader>
+							{/if}
+						{:else}
 							<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
 								<Spinner className=" size-4" />
 								<div class=" ">Loading...</div>
 							</div>
-						</Loader>
-					{/if}
-				{:else}
-					<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
-						<Spinner className=" size-4" />
-						<div class=" ">Loading...</div>
+						{/if}
 					</div>
-				{/if}
+				</Folder>
 			</div>
 		</div>
 

+ 12 - 4
src/lib/components/layout/Sidebar/ChatItem.svelte

@@ -104,6 +104,14 @@
 	const onDragStart = (event) => {
 		event.dataTransfer.setDragImage(dragImage, 0, 0);
 
+		// Set the data to be transferred
+		event.dataTransfer.setData(
+			'text/plain',
+			JSON.stringify({
+				id: chat.id
+			})
+		);
+
 		drag = true;
 		itemElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
 	};
@@ -114,8 +122,8 @@
 	};
 
 	const onDragEnd = (event) => {
-		drag = false;
 		itemElement.style.opacity = '1'; // Reset visual cue after drag
+		drag = false;
 	};
 
 	onMount(() => {
@@ -142,9 +150,9 @@
 
 {#if drag && x && y}
 	<DragGhost {x} {y}>
-		<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg">
+		<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-44">
 			<div>
-				<div class=" text-xs text-white">
+				<div class=" text-xs text-white line-clamp-1">
 					{chat.title}
 				</div>
 			</div>
@@ -169,7 +177,7 @@
 		</div>
 	{:else}
 		<a
-			class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
+			class=" w-full flex justify-between rounded-lg px-3 py-2 {chat.id === $chatId || confirmEdit
 				? 'bg-gray-200 dark:bg-gray-900'
 				: selected
 					? 'bg-gray-100 dark:bg-gray-950'