forked from open-webui/open-webui
		
	feat: # to import doc
This commit is contained in:
		
							parent
							
								
									2603ac30bc
								
							
						
					
					
						commit
						cc3f84f916
					
				
					 11 changed files with 894 additions and 32 deletions
				
			
		
							
								
								
									
										177
									
								
								src/lib/apis/documents/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								src/lib/apis/documents/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,177 @@ | |||
| import { WEBUI_API_BASE_URL } from '$lib/constants'; | ||||
| 
 | ||||
| export const createNewDoc = async ( | ||||
| 	token: string, | ||||
| 	collection_name: string, | ||||
| 	filename: string, | ||||
| 	name: string, | ||||
| 	title: string | ||||
| ) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/create`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
| 			collection_name: collection_name, | ||||
| 			filename: filename, | ||||
| 			name: name, | ||||
| 			title: title | ||||
| 		}) | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err.detail; | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const getDocs = async (token: string = '') => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/`, { | ||||
| 		method: 'GET', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			authorization: `Bearer ${token}` | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err.detail; | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const getDocByName = async (token: string, name: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}`, { | ||||
| 		method: 'GET', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			authorization: `Bearer ${token}` | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err.detail; | ||||
| 
 | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| type DocUpdateForm = { | ||||
| 	name: string; | ||||
| 	title: string; | ||||
| }; | ||||
| 
 | ||||
| export const updateDocByName = async (token: string, name: string, form: DocUpdateForm) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/name/${name}/update`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
| 			name: form.name, | ||||
| 			title: form.title | ||||
| 		}) | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err.detail; | ||||
| 
 | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const deleteDocByName = async (token: string, name: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}/delete`, { | ||||
| 		method: 'DELETE', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			authorization: `Bearer ${token}` | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err.detail; | ||||
| 
 | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
							
								
								
									
										6
									
								
								src/lib/components/AddFilesPlaceholder.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/lib/components/AddFilesPlaceholder.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| <div class="  text-center text-6xl mb-3">📄</div> | ||||
| <div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div> | ||||
| 
 | ||||
| <div class=" mt-2 text-center text-sm dark:text-gray-200 w-full"> | ||||
| 	Drop any files here to add to the conversation | ||||
| </div> | ||||
|  | @ -7,6 +7,9 @@ | |||
| 	import Prompts from './MessageInput/PromptCommands.svelte'; | ||||
| 	import Suggestions from './MessageInput/Suggestions.svelte'; | ||||
| 	import { uploadDocToVectorDB } from '$lib/apis/rag'; | ||||
| 	import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte'; | ||||
| 	import { SUPPORTED_FILE_TYPE } from '$lib/constants'; | ||||
| 	import Documents from './MessageInput/Documents.svelte'; | ||||
| 
 | ||||
| 	export let submitPrompt: Function; | ||||
| 	export let stopResponse: Function; | ||||
|  | @ -16,6 +19,7 @@ | |||
| 
 | ||||
| 	let filesInputElement; | ||||
| 	let promptsElement; | ||||
| 	let documentsElement; | ||||
| 
 | ||||
| 	let inputFiles; | ||||
| 	let dragged = false; | ||||
|  | @ -143,14 +147,7 @@ | |||
| 					const file = inputFiles[0]; | ||||
| 					if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) { | ||||
| 						reader.readAsDataURL(file); | ||||
| 					} else if ( | ||||
| 						[ | ||||
| 							'application/pdf', | ||||
| 							'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | ||||
| 							'text/plain', | ||||
| 							'text/csv' | ||||
| 						].includes(file['type']) | ||||
| 					) { | ||||
| 					} else if (SUPPORTED_FILE_TYPE.includes(file['type'])) { | ||||
| 						uploadDoc(file); | ||||
| 					} else { | ||||
| 						toast.error(`Unsupported File Type '${file['type']}'.`); | ||||
|  | @ -179,12 +176,7 @@ | |||
| 		<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center"> | ||||
| 			<div class="m-auto pt-64 flex flex-col justify-center"> | ||||
| 				<div class="max-w-md"> | ||||
| 					<div class="  text-center text-6xl mb-3">🗂️</div> | ||||
| 					<div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div> | ||||
| 
 | ||||
| 					<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full"> | ||||
| 						Drop any files/images here to add to the conversation | ||||
| 					</div> | ||||
| 					<AddFilesPlaceholder /> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | @ -224,6 +216,22 @@ | |||
| 			<div class="w-full"> | ||||
| 				{#if prompt.charAt(0) === '/'} | ||||
| 					<Prompts bind:this={promptsElement} bind:prompt /> | ||||
| 				{:else if prompt.charAt(0) === '#'} | ||||
| 					<Documents | ||||
| 						bind:this={documentsElement} | ||||
| 						bind:prompt | ||||
| 						on:select={(e) => { | ||||
| 							console.log(e); | ||||
| 							files = [ | ||||
| 								...files, | ||||
| 								{ | ||||
| 									type: 'doc', | ||||
| 									...e.detail, | ||||
| 									upload_status: true | ||||
| 								} | ||||
| 							]; | ||||
| 						}} | ||||
| 					/> | ||||
| 				{:else if messages.length == 0 && suggestionPrompts.length !== 0} | ||||
| 					<Suggestions {suggestionPrompts} {submitPrompt} /> | ||||
| 				{/if} | ||||
|  | @ -256,14 +264,7 @@ | |||
| 							const file = inputFiles[0]; | ||||
| 							if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) { | ||||
| 								reader.readAsDataURL(file); | ||||
| 							} else if ( | ||||
| 								[ | ||||
| 									'application/pdf', | ||||
| 									'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | ||||
| 									'text/plain', | ||||
| 									'text/csv' | ||||
| 								].includes(file['type']) | ||||
| 							) { | ||||
| 							} else if (SUPPORTED_FILE_TYPE.includes(file['type'])) { | ||||
| 								uploadDoc(file); | ||||
| 								filesInputElement.value = ''; | ||||
| 							} else { | ||||
|  | @ -448,8 +449,10 @@ | |||
| 									editButton?.click(); | ||||
| 								} | ||||
| 
 | ||||
| 								if (prompt.charAt(0) === '/' && e.key === 'ArrowUp') { | ||||
| 									promptsElement.selectUp(); | ||||
| 								if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') { | ||||
| 									e.preventDefault(); | ||||
| 
 | ||||
| 									(promptsElement || documentsElement).selectUp(); | ||||
| 
 | ||||
| 									const commandOptionButton = [ | ||||
| 										...document.getElementsByClassName('selected-command-option-button') | ||||
|  | @ -457,8 +460,10 @@ | |||
| 									commandOptionButton.scrollIntoView({ block: 'center' }); | ||||
| 								} | ||||
| 
 | ||||
| 								if (prompt.charAt(0) === '/' && e.key === 'ArrowDown') { | ||||
| 									promptsElement.selectDown(); | ||||
| 								if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') { | ||||
| 									e.preventDefault(); | ||||
| 
 | ||||
| 									(promptsElement || documentsElement).selectDown(); | ||||
| 
 | ||||
| 									const commandOptionButton = [ | ||||
| 										...document.getElementsByClassName('selected-command-option-button') | ||||
|  | @ -466,7 +471,7 @@ | |||
| 									commandOptionButton.scrollIntoView({ block: 'center' }); | ||||
| 								} | ||||
| 
 | ||||
| 								if (prompt.charAt(0) === '/' && e.key === 'Enter') { | ||||
| 								if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Enter') { | ||||
| 									e.preventDefault(); | ||||
| 
 | ||||
| 									const commandOptionButton = [ | ||||
|  | @ -476,7 +481,7 @@ | |||
| 									commandOptionButton?.click(); | ||||
| 								} | ||||
| 
 | ||||
| 								if (prompt.charAt(0) === '/' && e.key === 'Tab') { | ||||
| 								if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Tab') { | ||||
| 									e.preventDefault(); | ||||
| 
 | ||||
| 									const commandOptionButton = [ | ||||
|  |  | |||
							
								
								
									
										78
									
								
								src/lib/components/chat/MessageInput/Documents.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/lib/components/chat/MessageInput/Documents.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | |||
| <script lang="ts"> | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 
 | ||||
| 	import { documents } from '$lib/stores'; | ||||
| 	import { removeFirstHashWord } from '$lib/utils'; | ||||
| 	import { tick } from 'svelte'; | ||||
| 
 | ||||
| 	export let prompt = ''; | ||||
| 
 | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 	let selectedIdx = 0; | ||||
| 	let filteredDocs = []; | ||||
| 
 | ||||
| 	$: filteredDocs = $documents | ||||
| 		.filter((p) => p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? '')) | ||||
| 		.sort((a, b) => a.title.localeCompare(b.title)); | ||||
| 
 | ||||
| 	$: if (prompt) { | ||||
| 		selectedIdx = 0; | ||||
| 	} | ||||
| 
 | ||||
| 	export const selectUp = () => { | ||||
| 		selectedIdx = Math.max(0, selectedIdx - 1); | ||||
| 	}; | ||||
| 
 | ||||
| 	export const selectDown = () => { | ||||
| 		selectedIdx = Math.min(selectedIdx + 1, filteredDocs.length - 1); | ||||
| 	}; | ||||
| 
 | ||||
| 	const confirmSelect = async (doc) => { | ||||
| 		dispatch('select', doc); | ||||
| 
 | ||||
| 		prompt = removeFirstHashWord(prompt); | ||||
| 		const chatInputElement = document.getElementById('chat-textarea'); | ||||
| 
 | ||||
| 		await tick(); | ||||
| 		chatInputElement?.focus(); | ||||
| 		await tick(); | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| {#if filteredDocs.length > 0} | ||||
| 	<div class="md:px-2 mb-3 text-left w-full"> | ||||
| 		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700"> | ||||
| 			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center"> | ||||
| 				<div class=" text-lg font-semibold mt-2">#</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="max-h-60 flex flex-col w-full rounded-r-lg"> | ||||
| 				<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5"> | ||||
| 					{#each filteredDocs as doc, docIdx} | ||||
| 						<button | ||||
| 							class=" px-3 py-1.5 rounded-lg w-full text-left {docIdx === selectedIdx | ||||
| 								? ' bg-gray-100 selected-command-option-button' | ||||
| 								: ''}" | ||||
| 							type="button" | ||||
| 							on:click={() => { | ||||
| 								confirmSelect(doc); | ||||
| 							}} | ||||
| 							on:mousemove={() => { | ||||
| 								selectedIdx = docIdx; | ||||
| 							}} | ||||
| 							on:focus={() => {}} | ||||
| 						> | ||||
| 							<div class=" font-medium text-black line-clamp-1"> | ||||
| 								#{doc.name} ({doc.filename}) | ||||
| 							</div> | ||||
| 
 | ||||
| 							<div class=" text-xs text-gray-600 line-clamp-1"> | ||||
| 								{doc.title} | ||||
| 							</div> | ||||
| 						</button> | ||||
| 					{/each} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/if} | ||||
|  | @ -11,6 +11,13 @@ export const WEB_UI_VERSION = 'v1.0.0-alpha-static'; | |||
| 
 | ||||
| export const REQUIRED_OLLAMA_VERSION = '0.1.16'; | ||||
| 
 | ||||
| export const SUPPORTED_FILE_TYPE = [ | ||||
| 	'application/pdf', | ||||
| 	'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | ||||
| 	'text/plain', | ||||
| 	'text/csv' | ||||
| ]; | ||||
| 
 | ||||
| // Source: https://kit.svelte.dev/docs/modules#$env-static-public
 | ||||
| // This feature, akin to $env/static/private, exclusively incorporates environment variables
 | ||||
| // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
 | ||||
|  |  | |||
|  | @ -11,8 +11,23 @@ export const chatId = writable(''); | |||
| 
 | ||||
| export const chats = writable([]); | ||||
| export const models = writable([]); | ||||
| 
 | ||||
| export const modelfiles = writable([]); | ||||
| export const prompts = writable([]); | ||||
| export const documents = writable([ | ||||
| 	{ | ||||
| 		collection_name: 'collection_name', | ||||
| 		filename: 'filename', | ||||
| 		name: 'name', | ||||
| 		title: 'title' | ||||
| 	}, | ||||
| 	{ | ||||
| 		collection_name: 'collection_name1', | ||||
| 		filename: 'filename1', | ||||
| 		name: 'name1', | ||||
| 		title: 'title1' | ||||
| 	} | ||||
| ]); | ||||
| 
 | ||||
| export const settings = writable({}); | ||||
| export const showSettings = writable(false); | ||||
|  |  | |||
|  | @ -128,6 +128,24 @@ export const findWordIndices = (text) => { | |||
| 	return matches; | ||||
| }; | ||||
| 
 | ||||
| export const removeFirstHashWord = (inputString) => { | ||||
| 	// Split the string into an array of words
 | ||||
| 	const words = inputString.split(' '); | ||||
| 
 | ||||
| 	// Find the index of the first word that starts with #
 | ||||
| 	const index = words.findIndex((word) => word.startsWith('#')); | ||||
| 
 | ||||
| 	// Remove the first word with #
 | ||||
| 	if (index !== -1) { | ||||
| 		words.splice(index, 1); | ||||
| 	} | ||||
| 
 | ||||
| 	// Join the remaining words back into a string
 | ||||
| 	const resultString = words.join(' '); | ||||
| 
 | ||||
| 	return resultString; | ||||
| }; | ||||
| 
 | ||||
| export const calculateSHA256 = async (file) => { | ||||
| 	// Create a FileReader to read the file asynchronously
 | ||||
| 	const reader = new FileReader(); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek