Browse Source

enh: drag and drop chat exports

Timothy J. Baek 6 tháng trước cách đây
mục cha
commit
b38e2fab32

+ 20 - 13
src/lib/components/AddFilesPlaceholder.svelte

@@ -2,20 +2,27 @@
 	import { getContext } from 'svelte';
 
 	export let title = '';
+	export let content = '';
 	const i18n = getContext('i18n');
 </script>
 
-<div class="  text-center text-6xl mb-3">📄</div>
-<div class="text-center dark:text-white text-2xl font-semibold z-50">
-	{#if title}
-		{title}
-	{:else}
-		{$i18n.t('Add Files')}
-	{/if}
-</div>
-
-<slot
-	><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
-		{$i18n.t('Drop any files here to add to the conversation')}
+<div class="px-3">
+	<div class="text-center text-6xl mb-3">📄</div>
+	<div class="text-center dark:text-white text-xl font-semibold z-50">
+		{#if title}
+			{title}
+		{:else}
+			{$i18n.t('Add Files')}
+		{/if}
 	</div>
-</slot>
+
+	<slot
+		><div class="px-2 mt-2 text-center text-sm dark:text-gray-200 w-full">
+			{#if content}
+				{content}
+			{:else}
+				{$i18n.t('Drop any files here to add to the conversation')}
+			{/if}
+		</div>
+	</slot>
+</div>

+ 155 - 73
src/lib/components/layout/Sidebar.svelte

@@ -19,7 +19,7 @@
 		showOverview,
 		showControls
 	} from '$lib/stores';
-	import { onMount, getContext, tick } from 'svelte';
+	import { onMount, getContext, tick, onDestroy } from 'svelte';
 
 	const i18n = getContext('i18n');
 
@@ -33,7 +33,8 @@
 		getAllChatTags,
 		archiveChatById,
 		cloneChatById,
-		getChatListBySearchText
+		getChatListBySearchText,
+		createNewChat
 	} from '$lib/apis/chats';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
@@ -43,6 +44,8 @@
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import Spinner from '../common/Spinner.svelte';
 	import Loader from '../common/Loader.svelte';
+	import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
+	import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
 
 	const BREAKPOINT = 768;
 
@@ -115,6 +118,127 @@
 		}
 	};
 
+	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('/');
+			}
+
+			allChatsLoaded = false;
+			currentChatPage.set(1);
+			await chats.set(await getChatList(localStorage.token, $currentChatPage));
+
+			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
+		}
+	};
+
+	const inputFilesHandler = async (files) => {
+		console.log(files);
+
+		for (const file of files) {
+			const reader = new FileReader();
+			reader.onload = async (e) => {
+				const content = e.target.result;
+
+				try {
+					const items = JSON.parse(content);
+
+					for (const item of items) {
+						if (item.chat) {
+							await createNewChat(localStorage.token, item.chat);
+						}
+					}
+				} catch {
+					toast.error($i18n.t(`Invalid file format.`));
+				}
+
+				initChatList();
+			};
+
+			reader.readAsText(file);
+		}
+	};
+
+	let dragged = false;
+
+	const onDragOver = (e) => {
+		e.preventDefault();
+		dragged = true;
+	};
+
+	const onDragLeave = () => {
+		dragged = false;
+	};
+
+	const onDrop = async (e) => {
+		e.preventDefault();
+		console.log(e);
+
+		if (e.dataTransfer?.files) {
+			const inputFiles = Array.from(e.dataTransfer?.files);
+			if (inputFiles && inputFiles.length > 0) {
+				console.log(inputFiles);
+				inputFilesHandler(inputFiles);
+			} else {
+				toast.error($i18n.t(`File not found.`));
+			}
+		}
+
+		dragged = false;
+	};
+
+	let touchstart;
+	let touchend;
+
+	function checkDirection() {
+		const screenWidth = window.innerWidth;
+		const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
+		if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
+			if (touchend.screenX < touchstart.screenX) {
+				showSidebar.set(false);
+			}
+			if (touchend.screenX > touchstart.screenX) {
+				showSidebar.set(true);
+			}
+		}
+	}
+
+	const onTouchStart = (e) => {
+		touchstart = e.changedTouches[0];
+		console.log(touchstart.clientX);
+	};
+
+	const onTouchEnd = (e) => {
+		touchend = e.changedTouches[0];
+		checkDirection();
+	};
+
+	const onKeyDown = (e) => {
+		if (e.key === 'Shift') {
+			shiftKey = true;
+		}
+	};
+
+	const onKeyUp = (e) => {
+		if (e.key === 'Shift') {
+			shiftKey = false;
+		}
+	};
+
+	const onFocus = () => {};
+
+	const onBlur = () => {
+		shiftKey = false;
+		selectedChatId = null;
+	};
+
 	onMount(async () => {
 		mobile.subscribe((e) => {
 			if ($showSidebar && e) {
@@ -134,51 +258,6 @@
 		await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
 		await initChatList();
 
-		let touchstart;
-		let touchend;
-
-		function checkDirection() {
-			const screenWidth = window.innerWidth;
-			const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
-			if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
-				if (touchend.screenX < touchstart.screenX) {
-					showSidebar.set(false);
-				}
-				if (touchend.screenX > touchstart.screenX) {
-					showSidebar.set(true);
-				}
-			}
-		}
-
-		const onTouchStart = (e) => {
-			touchstart = e.changedTouches[0];
-			console.log(touchstart.clientX);
-		};
-
-		const onTouchEnd = (e) => {
-			touchend = e.changedTouches[0];
-			checkDirection();
-		};
-
-		const onKeyDown = (e) => {
-			if (e.key === 'Shift') {
-				shiftKey = true;
-			}
-		};
-
-		const onKeyUp = (e) => {
-			if (e.key === 'Shift') {
-				shiftKey = false;
-			}
-		};
-
-		const onFocus = () => {};
-
-		const onBlur = () => {
-			shiftKey = false;
-			selectedChatId = null;
-		};
-
 		window.addEventListener('keydown', onKeyDown);
 		window.addEventListener('keyup', onKeyUp);
 
@@ -188,38 +267,29 @@
 		window.addEventListener('focus', onFocus);
 		window.addEventListener('blur', onBlur);
 
-		return () => {
-			window.removeEventListener('keydown', onKeyDown);
-			window.removeEventListener('keyup', onKeyUp);
-
-			window.removeEventListener('touchstart', onTouchStart);
-			window.removeEventListener('touchend', onTouchEnd);
+		const dropZone = document.getElementById('sidebar');
 
-			window.removeEventListener('focus', onFocus);
-			window.removeEventListener('blur', onBlur);
-		};
+		dropZone?.addEventListener('dragover', onDragOver);
+		dropZone?.addEventListener('drop', onDrop);
+		dropZone?.addEventListener('dragleave', onDragLeave);
 	});
 
-	const deleteChatHandler = async (id) => {
-		const res = await deleteChatById(localStorage.token, id).catch((error) => {
-			toast.error(error);
-			return null;
-		});
+	onDestroy(() => {
+		window.removeEventListener('keydown', onKeyDown);
+		window.removeEventListener('keyup', onKeyUp);
 
-		if (res) {
-			if ($chatId === id) {
-				await chatId.set('');
-				await tick();
-				goto('/');
-			}
+		window.removeEventListener('touchstart', onTouchStart);
+		window.removeEventListener('touchend', onTouchEnd);
 
-			allChatsLoaded = false;
-			currentChatPage.set(1);
-			await chats.set(await getChatList(localStorage.token, $currentChatPage));
+		window.removeEventListener('focus', onFocus);
+		window.removeEventListener('blur', onBlur);
 
-			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
-		}
-	};
+		const dropZone = document.getElementById('sidebar');
+
+		dropZone?.removeEventListener('dragover', onDragOver);
+		dropZone?.removeEventListener('drop', onDrop);
+		dropZone?.removeEventListener('dragleave', onDragLeave);
+	});
 </script>
 
 <ArchivedChatsModal
@@ -261,6 +331,18 @@
         "
 	data-state={$showSidebar}
 >
+	{#if dragged}
+		<div
+			class="absolute w-full h-full max-h-full backdrop-blur bg-gray-800/40 flex justify-center z-[999] touch-none pointer-events-none"
+		>
+			<div class="m-auto pt-64 flex flex-col justify-center">
+				<AddFilesPlaceholder
+					title={$i18n.t('Drop Chat Export')}
+					content={$i18n.t('Drop a chat export file here to import it.')}
+				/>
+			</div>
+		</div>
+	{/if}
 	<div
 		class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar
 			? ''