forked from open-webui/open-webui
		
	feat: batch doc tagging
This commit is contained in:
		
							parent
							
								
									63628a70a6
								
							
						
					
					
						commit
						92c922b061
					
				
					 3 changed files with 199 additions and 36 deletions
				
			
		|  | @ -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) => { | ||||||
|  |  | ||||||
							
								
								
									
										188
									
								
								src/lib/components/documents/AddDocModal.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/lib/components/documents/AddDocModal.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||||
|  | @ -16,6 +16,7 @@ | ||||||
| 	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 = ''; | ||||||
|  | @ -24,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 = ''; | ||||||
|  | @ -171,36 +173,7 @@ | ||||||
| 	<EditDocModal bind:show={showEditDocModal} {selectedDoc} /> | 	<EditDocModal bind:show={showEditDocModal} {selectedDoc} /> | ||||||
| {/key} | {/key} | ||||||
| 
 | 
 | ||||||
| <input | <AddDocModal bind:show={showAddDocModal} /> | ||||||
| 	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.`); |  | ||||||
| 		} |  | ||||||
| 	}} |  | ||||||
| /> |  | ||||||
| 
 | 
 | ||||||
| <SettingsModal bind:show={showSettingsModal} /> | <SettingsModal bind:show={showSettingsModal} /> | ||||||
| 
 | 
 | ||||||
|  | @ -268,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 | ||||||
|  | @ -358,15 +331,15 @@ | ||||||
| 							</div> | 							</div> | ||||||
| 
 | 
 | ||||||
| 							<div class="flex gap-1"> | 							<div class="flex gap-1"> | ||||||
| 								<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 = ''; | 										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">add tags</div> | ||||||
| 								</button> | 								</button> --> | ||||||
| 
 | 
 | ||||||
| 								<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" | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek