Ver Fonte

Merge pull request #919 from open-webui/dev

0.1.105
Timothy Jaeryang Baek há 1 ano atrás
pai
commit
6df2505bf0

+ 10 - 0
CHANGELOG.md

@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
+## [0.1.105] - 2024-02-25
+
+### Added
+
+- **📄 Document Selection**: Now you can select and delete multiple documents at once for easier management.
+
+### Changed
+
+- **🏷️ Document Pre-tagging**: Simply click the "+" button at the top, enter tag names in the popup window, or select from a list of existing tags. Then, upload files with the added tags for streamlined organization.
+
 ## [0.1.104] - 2024-02-25
 ## [0.1.104] - 2024-02-25
 
 
 ### Added
 ### Added

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
 	"name": "open-webui",
 	"name": "open-webui",
-	"version": "0.1.104",
+	"version": "0.1.105",
 	"private": true,
 	"private": true,
 	"scripts": {
 	"scripts": {
 		"dev": "vite dev --host",
 		"dev": "vite dev --host",

+ 4 - 2
src/lib/apis/documents/index.ts

@@ -5,7 +5,8 @@ export const createNewDoc = async (
 	collection_name: string,
 	collection_name: string,
 	filename: string,
 	filename: string,
 	name: string,
 	name: string,
-	title: string
+	title: string,
+	content: object | null = null
 ) => {
 ) => {
 	let error = null;
 	let error = null;
 
 
@@ -20,7 +21,8 @@ export const createNewDoc = async (
 			collection_name: collection_name,
 			collection_name: collection_name,
 			filename: filename,
 			filename: filename,
 			name: name,
 			name: name,
-			title: title
+			title: title,
+			...(content ? { content: JSON.stringify(content) } : {})
 		})
 		})
 	})
 	})
 		.then(async (res) => {
 		.then(async (res) => {

+ 0 - 1
src/lib/apis/openai/index.ts

@@ -150,7 +150,6 @@ export const getOpenAIModels = async (token: string = '') => {
 			return res.json();
 			return res.json();
 		})
 		})
 		.catch((err) => {
 		.catch((err) => {
-			console.log(err);
 			error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
 			error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
 			return [];
 			return [];
 		});
 		});

+ 70 - 0
src/lib/components/common/Checkbox.svelte

@@ -0,0 +1,70 @@
+<script lang="ts">
+	import { createEventDispatcher } from 'svelte';
+	const dispatch = createEventDispatcher();
+
+	export let state = 'unchecked';
+	export let indeterminate = false;
+
+	let _state = 'unchecked';
+
+	$: _state = state;
+</script>
+
+<button
+	class=" outline -outline-offset-1 outline-[1.5px] outline-gray-200 dark:outline-gray-600 {state !==
+	'unchecked'
+		? 'bg-black outline-black '
+		: 'hover:outline-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800'} text-white transition-all rounded inline-block w-3.5 h-3.5 relative"
+	on:click={() => {
+		if (_state === 'unchecked') {
+			_state = 'checked';
+			dispatch('change', _state);
+		} else if (_state === 'checked') {
+			_state = 'unchecked';
+			if (!indeterminate) {
+				dispatch('change', _state);
+			}
+		} else if (indeterminate) {
+			_state = 'checked';
+			dispatch('change', _state);
+		}
+	}}
+>
+	<div class="top-0 left-0 absolute w-full flex justify-center">
+		{#if _state === 'checked'}
+			<svg
+				class="w-3.5 h-3.5"
+				aria-hidden="true"
+				xmlns="http://www.w3.org/2000/svg"
+				fill="none"
+				viewBox="0 0 24 24"
+			>
+				<path
+					stroke="currentColor"
+					stroke-linecap="round"
+					stroke-linejoin="round"
+					stroke-width="3"
+					d="m5 12 4.7 4.5 9.3-9"
+				/>
+			</svg>
+		{:else if indeterminate}
+			<svg
+				class="w-3 h-3.5 text-gray-800 dark:text-white"
+				aria-hidden="true"
+				xmlns="http://www.w3.org/2000/svg"
+				fill="none"
+				viewBox="0 0 24 24"
+			>
+				<path
+					stroke="currentColor"
+					stroke-linecap="round"
+					stroke-linejoin="round"
+					stroke-width="3"
+					d="M5 12h14"
+				/>
+			</svg>
+		{/if}
+	</div>
+
+	<!-- {checked} -->
+</button>

+ 188 - 0
src/lib/components/documents/AddDocModal.svelte

@@ -0,0 +1,188 @@
+<script lang="ts">
+	import toast from 'svelte-french-toast';
+	import dayjs from 'dayjs';
+	import { onMount } from 'svelte';
+
+	import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
+	import Modal from '../common/Modal.svelte';
+	import { documents } from '$lib/stores';
+	import TagInput from '../common/Tags/TagInput.svelte';
+	import Tags from '../common/Tags.svelte';
+	import { addTagById } from '$lib/apis/chats';
+	import { uploadDocToVectorDB } from '$lib/apis/rag';
+	import { transformFileName } from '$lib/utils';
+	import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants';
+
+	export let show = false;
+	export let selectedDoc;
+
+	let inputFiles;
+	let tags = [];
+
+	let doc = {
+		name: '',
+		title: '',
+		content: null
+	};
+
+	const uploadDoc = async (file) => {
+		const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			await createNewDoc(
+				localStorage.token,
+				res.collection_name,
+				res.filename,
+				transformFileName(res.filename),
+				res.filename,
+				tags.length > 0
+					? {
+							tags: tags
+					  }
+					: null
+			).catch((error) => {
+				toast.error(error);
+				return null;
+			});
+			await documents.set(await getDocs(localStorage.token));
+		}
+	};
+
+	const submitHandler = async () => {
+		if (inputFiles && inputFiles.length > 0) {
+			for (const file of inputFiles) {
+				console.log(file, file.name.split('.').at(-1));
+				if (
+					SUPPORTED_FILE_TYPE.includes(file['type']) ||
+					SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
+				) {
+					uploadDoc(file);
+				} else {
+					toast.error(
+						`Unknown File Type '${file['type']}', but accepting and treating as plain text`
+					);
+					uploadDoc(file);
+				}
+			}
+
+			inputFiles = null;
+			document.getElementById('upload-doc-input').value = '';
+		} else {
+			toast.error(`File not found.`);
+		}
+
+		show = false;
+		documents.set(await getDocs(localStorage.token));
+	};
+
+	const addTagHandler = async (tagName) => {
+		if (!tags.find((tag) => tag.name === tagName) && tagName !== '') {
+			tags = [...tags, { name: tagName }];
+		} else {
+			console.log('tag already exists');
+		}
+	};
+
+	const deleteTagHandler = async (tagName) => {
+		tags = tags.filter((tag) => tag.name !== tagName);
+	};
+
+	onMount(() => {});
+</script>
+
+<Modal size="sm" bind:show>
+	<div>
+		<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
+			<div class=" text-lg font-medium self-center">Add Docs</div>
+			<button
+				class="self-center"
+				on:click={() => {
+					show = false;
+				}}
+			>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 20 20"
+					fill="currentColor"
+					class="w-5 h-5"
+				>
+					<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>
+		<hr class=" dark:border-gray-800" />
+
+		<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
+			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
+				<form
+					class="flex flex-col w-full"
+					on:submit|preventDefault={() => {
+						submitHandler();
+					}}
+				>
+					<div class="mb-3 w-full">
+						<input id="upload-doc-input" hidden bind:files={inputFiles} type="file" multiple />
+
+						<button
+							class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
+							type="button"
+							on:click={() => {
+								document.getElementById('upload-doc-input')?.click();
+							}}
+						>
+							{#if inputFiles}
+								{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
+							{:else}
+								Click here to select documents.
+							{/if}
+						</button>
+					</div>
+
+					<div class=" flex flex-col space-y-1.5">
+						<div class="flex flex-col w-full">
+							<div class=" mb-1.5 text-xs text-gray-500">Tags</div>
+
+							<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
+						</div>
+					</div>
+
+					<div class="flex justify-end pt-5 text-sm font-medium">
+						<button
+							class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
+							type="submit"
+						>
+							Save
+						</button>
+					</div>
+				</form>
+			</div>
+		</div>
+	</div>
+</Modal>
+
+<style>
+	input::-webkit-outer-spin-button,
+	input::-webkit-inner-spin-button {
+		/* display: none; <- Crashes Chrome on hover */
+		-webkit-appearance: none;
+		margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
+	}
+
+	.tabs::-webkit-scrollbar {
+		display: none; /* for Chrome, Safari and Opera */
+	}
+
+	.tabs {
+		-ms-overflow-style: none; /* IE and Edge */
+		scrollbar-width: none; /* Firefox */
+	}
+
+	input[type='number'] {
+		-moz-appearance: textfield; /* Firefox */
+	}
+</style>

+ 0 - 7
src/routes/(app)/+page.svelte

@@ -104,13 +104,8 @@
 			await cancelChatCompletion(localStorage.token, currentRequestId);
 			await cancelChatCompletion(localStorage.token, currentRequestId);
 			currentRequestId = null;
 			currentRequestId = null;
 		}
 		}
-
 		window.history.replaceState(history.state, '', `/`);
 		window.history.replaceState(history.state, '', `/`);
-
-		console.log('initNewChat');
-
 		await chatId.set('');
 		await chatId.set('');
-		console.log($chatId);
 
 
 		autoScroll = true;
 		autoScroll = true;
 
 
@@ -121,8 +116,6 @@
 			currentId: null
 			currentId: null
 		};
 		};
 
 
-		console.log($config);
-
 		if ($page.url.searchParams.get('models')) {
 		if ($page.url.searchParams.get('models')) {
 			selectedModels = $page.url.searchParams.get('models')?.split(',');
 			selectedModels = $page.url.searchParams.get('models')?.split(',');
 		} else if ($settings?.models) {
 		} else if ($settings?.models) {

+ 110 - 55
src/routes/(app)/documents/+page.svelte

@@ -11,9 +11,12 @@
 	import { uploadDocToVectorDB } from '$lib/apis/rag';
 	import { uploadDocToVectorDB } from '$lib/apis/rag';
 	import { transformFileName } from '$lib/utils';
 	import { transformFileName } from '$lib/utils';
 
 
+	import Checkbox from '$lib/components/common/Checkbox.svelte';
+
 	import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
 	import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
 	import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
 	import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
 	import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
 	import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
+	import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
 	let importFiles = '';
 	let importFiles = '';
 
 
 	let inputFiles = '';
 	let inputFiles = '';
@@ -22,6 +25,7 @@
 	let tags = [];
 	let tags = [];
 
 
 	let showSettingsModal = false;
 	let showSettingsModal = false;
+	let showAddDocModal = false;
 	let showEditDocModal = false;
 	let showEditDocModal = false;
 	let selectedDoc;
 	let selectedDoc;
 	let selectedTag = '';
 	let selectedTag = '';
@@ -33,6 +37,16 @@
 		await documents.set(await getDocs(localStorage.token));
 		await documents.set(await getDocs(localStorage.token));
 	};
 	};
 
 
+	const deleteDocs = async (docs) => {
+		const res = await Promise.all(
+			docs.map(async (doc) => {
+				return await deleteDocByName(localStorage.token, doc.name);
+			})
+		);
+
+		await documents.set(await getDocs(localStorage.token));
+	};
+
 	const uploadDoc = async (file) => {
 	const uploadDoc = async (file) => {
 		const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
 		const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
 			toast.error(error);
 			toast.error(error);
@@ -123,6 +137,15 @@
 			dropZone?.removeEventListener('dragleave', onDragLeave);
 			dropZone?.removeEventListener('dragleave', onDragLeave);
 		};
 		};
 	});
 	});
+
+	let filteredDocs;
+
+	$: filteredDocs = $documents.filter(
+		(doc) =>
+			(selectedTag === '' ||
+				(doc?.content?.tags ?? []).map((tag) => tag.name).includes(selectedTag)) &&
+			(query === '' || doc.name.includes(query))
+	);
 </script>
 </script>
 
 
 {#if dragged}
 {#if dragged}
@@ -150,36 +173,7 @@
 	<EditDocModal bind:show={showEditDocModal} {selectedDoc} />
 	<EditDocModal bind:show={showEditDocModal} {selectedDoc} />
 {/key}
 {/key}
 
 
-<input
-	id="upload-doc-input"
-	bind:files={inputFiles}
-	type="file"
-	multiple
-	hidden
-	on:change={async (e) => {
-		if (inputFiles && inputFiles.length > 0) {
-			for (const file of inputFiles) {
-				console.log(file, file.name.split('.').at(-1));
-				if (
-					SUPPORTED_FILE_TYPE.includes(file['type']) ||
-					SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
-				) {
-					uploadDoc(file);
-				} else {
-					toast.error(
-						`Unknown File Type '${file['type']}', but accepting and treating as plain text`
-					);
-					uploadDoc(file);
-				}
-			}
-
-			inputFiles = null;
-			e.target.value = '';
-		} else {
-			toast.error(`File not found.`);
-		}
-	}}
-/>
+<AddDocModal bind:show={showAddDocModal} />
 
 
 <SettingsModal bind:show={showSettingsModal} />
 <SettingsModal bind:show={showSettingsModal} />
 
 
@@ -247,7 +241,7 @@
 					<button
 					<button
 						class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
 						class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
 						on:click={() => {
 						on:click={() => {
-							document.getElementById('upload-doc-input')?.click();
+							showAddDocModal = true;
 						}}
 						}}
 					>
 					>
 						<svg
 						<svg
@@ -287,38 +281,96 @@
 
 
 			{#if tags.length > 0}
 			{#if tags.length > 0}
 				<div class="px-2.5 pt-1 flex gap-1 flex-wrap">
 				<div class="px-2.5 pt-1 flex gap-1 flex-wrap">
-					<button
-						class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
-						on:click={async () => {
-							selectedTag = '';
-							// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
-						}}
-					>
-						<div class=" text-xs font-medium self-center line-clamp-1">all</div>
-					</button>
-					{#each tags as tag}
+					<div class="ml-0.5 pr-3 my-auto flex items-center">
+						<Checkbox
+							state={filteredDocs.filter((doc) => doc?.selected === 'checked').length ===
+							filteredDocs.length
+								? 'checked'
+								: 'unchecked'}
+							indeterminate={filteredDocs.filter((doc) => doc?.selected === 'checked').length > 0 &&
+								filteredDocs.filter((doc) => doc?.selected === 'checked').length !==
+									filteredDocs.length}
+							on:change={(e) => {
+								if (e.detail === 'checked') {
+									filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'checked' }));
+								} else if (e.detail === 'unchecked') {
+									filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'unchecked' }));
+								}
+							}}
+						/>
+					</div>
+
+					{#if filteredDocs.filter((doc) => doc?.selected === 'checked').length === 0}
 						<button
 						<button
 							class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
 							class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
 							on:click={async () => {
 							on:click={async () => {
-								selectedTag = tag;
+								selectedTag = '';
 								// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
 								// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
 							}}
 							}}
 						>
 						>
-							<div class=" text-xs font-medium self-center line-clamp-1">
-								#{tag}
-							</div>
+							<div class=" text-xs font-medium self-center line-clamp-1">all</div>
 						</button>
 						</button>
-					{/each}
+
+						{#each tags as tag}
+							<button
+								class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
+								on:click={async () => {
+									selectedTag = tag;
+									// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
+								}}
+							>
+								<div class=" text-xs font-medium self-center line-clamp-1">
+									#{tag}
+								</div>
+							</button>
+						{/each}
+					{:else}
+						<div class="flex-1 flex w-full justify-between items-center">
+							<div class="text-xs font-medium py-0.5 self-center mr-1">
+								{filteredDocs.filter((doc) => doc?.selected === 'checked').length} Selected
+							</div>
+
+							<div class="flex gap-1">
+								<!-- <button
+									class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
+									on:click={async () => {
+										selectedTag = '';
+										// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
+									}}
+								>
+									<div class=" text-xs font-medium self-center line-clamp-1">add tags</div>
+								</button> -->
+
+								<button
+									class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
+									on:click={async () => {
+										deleteDocs(filteredDocs.filter((doc) => doc.selected === 'checked'));
+										// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
+									}}
+								>
+									<div class=" text-xs font-medium self-center line-clamp-1">delete</div>
+								</button>
+							</div>
+						</div>
+					{/if}
 				</div>
 				</div>
 			{/if}
 			{/if}
 
 
 			<div class="my-3 mb-5">
 			<div class="my-3 mb-5">
-				{#each $documents.filter((doc) => (selectedTag === '' || (doc?.content?.tags ?? [])
-								.map((tag) => tag.name)
-								.includes(selectedTag)) && (query === '' || doc.name.includes(query))) as doc}
-					<div
-						class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
+				{#each filteredDocs as doc}
+					<button
+						class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
+						on:click={() => {
+							if (doc?.selected === 'checked') {
+								doc.selected = 'unchecked';
+							} else {
+								doc.selected = 'checked';
+							}
+						}}
 					>
 					>
+						<div class="my-auto flex items-center">
+							<Checkbox state={doc?.selected ?? 'unchecked'} />
+						</div>
 						<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
 						<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
 							<div class=" flex items-center space-x-3">
 							<div class=" flex items-center space-x-3">
 								<div class="p-2.5 bg-red-400 text-white rounded-lg">
 								<div class="p-2.5 bg-red-400 text-white rounded-lg">
@@ -387,9 +439,10 @@
 						</div>
 						</div>
 						<div class="flex flex-row space-x-1 self-center">
 						<div class="flex flex-row space-x-1 self-center">
 							<button
 							<button
-								class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
+								class="self-center w-fit text-sm z-20 px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 								type="button"
 								type="button"
-								on:click={async () => {
+								on:click={async (e) => {
+									e.stopPropagation();
 									showEditDocModal = !showEditDocModal;
 									showEditDocModal = !showEditDocModal;
 									selectedDoc = doc;
 									selectedDoc = doc;
 								}}
 								}}
@@ -435,7 +488,9 @@
 							<button
 							<button
 								class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 								class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
 								type="button"
 								type="button"
-								on:click={() => {
+								on:click={(e) => {
+									e.stopPropagation();
+
 									deleteDoc(doc.name);
 									deleteDoc(doc.name);
 								}}
 								}}
 							>
 							>
@@ -455,7 +510,7 @@
 								</svg>
 								</svg>
 							</button>
 							</button>
 						</div>
 						</div>
-					</div>
+					</button>
 				{/each}
 				{/each}
 			</div>
 			</div>