forked from open-webui/open-webui
		
	feat: external embeddings support
This commit is contained in:
		
							parent
							
								
									8b10b058e5
								
							
						
					
					
						commit
						2952e61167
					
				
					 6 changed files with 312 additions and 118 deletions
				
			
		|  | @ -654,6 +654,55 @@ async def generate_embeddings( | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def generate_ollama_embeddings( | ||||||
|  |     form_data: GenerateEmbeddingsForm, | ||||||
|  |     url_idx: Optional[int] = None, | ||||||
|  | ): | ||||||
|  |     if url_idx == None: | ||||||
|  |         model = form_data.model | ||||||
|  | 
 | ||||||
|  |         if ":" not in model: | ||||||
|  |             model = f"{model}:latest" | ||||||
|  | 
 | ||||||
|  |         if model in app.state.MODELS: | ||||||
|  |             url_idx = random.choice(app.state.MODELS[model]["urls"]) | ||||||
|  |         else: | ||||||
|  |             raise HTTPException( | ||||||
|  |                 status_code=400, | ||||||
|  |                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||||
|  |     log.info(f"url: {url}") | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         r = requests.request( | ||||||
|  |             method="POST", | ||||||
|  |             url=f"{url}/api/embeddings", | ||||||
|  |             data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|  |         ) | ||||||
|  |         r.raise_for_status() | ||||||
|  | 
 | ||||||
|  |         data = r.json() | ||||||
|  | 
 | ||||||
|  |         if "embedding" in data: | ||||||
|  |             return data["embedding"] | ||||||
|  |         else: | ||||||
|  |             raise "Something went wrong :/" | ||||||
|  |     except Exception as e: | ||||||
|  |         log.exception(e) | ||||||
|  |         error_detail = "Open WebUI: Server Connection Error" | ||||||
|  |         if r is not None: | ||||||
|  |             try: | ||||||
|  |                 res = r.json() | ||||||
|  |                 if "error" in res: | ||||||
|  |                     error_detail = f"Ollama: {res['error']}" | ||||||
|  |             except: | ||||||
|  |                 error_detail = f"Ollama: {e}" | ||||||
|  | 
 | ||||||
|  |         raise error_detail | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class GenerateCompletionForm(BaseModel): | class GenerateCompletionForm(BaseModel): | ||||||
|     model: str |     model: str | ||||||
|     prompt: str |     prompt: str | ||||||
|  |  | ||||||
|  | @ -39,13 +39,21 @@ import uuid | ||||||
| import json | import json | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | from apps.ollama.main import generate_ollama_embeddings | ||||||
|  | 
 | ||||||
| from apps.web.models.documents import ( | from apps.web.models.documents import ( | ||||||
|     Documents, |     Documents, | ||||||
|     DocumentForm, |     DocumentForm, | ||||||
|     DocumentResponse, |     DocumentResponse, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| from apps.rag.utils import query_doc, query_collection, get_embedding_model_path | from apps.rag.utils import ( | ||||||
|  |     query_doc, | ||||||
|  |     query_embeddings_doc, | ||||||
|  |     query_collection, | ||||||
|  |     query_embeddings_collection, | ||||||
|  |     get_embedding_model_path, | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| from utils.misc import ( | from utils.misc import ( | ||||||
|     calculate_sha256, |     calculate_sha256, | ||||||
|  | @ -58,6 +66,7 @@ from config import ( | ||||||
|     SRC_LOG_LEVELS, |     SRC_LOG_LEVELS, | ||||||
|     UPLOAD_DIR, |     UPLOAD_DIR, | ||||||
|     DOCS_DIR, |     DOCS_DIR, | ||||||
|  |     RAG_EMBEDDING_ENGINE, | ||||||
|     RAG_EMBEDDING_MODEL, |     RAG_EMBEDDING_MODEL, | ||||||
|     RAG_EMBEDDING_MODEL_AUTO_UPDATE, |     RAG_EMBEDDING_MODEL_AUTO_UPDATE, | ||||||
|     DEVICE_TYPE, |     DEVICE_TYPE, | ||||||
|  | @ -74,17 +83,20 @@ log.setLevel(SRC_LOG_LEVELS["RAG"]) | ||||||
| 
 | 
 | ||||||
| app = FastAPI() | app = FastAPI() | ||||||
| 
 | 
 | ||||||
| app.state.PDF_EXTRACT_IMAGES = False | 
 | ||||||
|  | app.state.TOP_K = 4 | ||||||
| app.state.CHUNK_SIZE = CHUNK_SIZE | app.state.CHUNK_SIZE = CHUNK_SIZE | ||||||
| app.state.CHUNK_OVERLAP = CHUNK_OVERLAP | app.state.CHUNK_OVERLAP = CHUNK_OVERLAP | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE | ||||||
|  | app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL | ||||||
| app.state.RAG_TEMPLATE = RAG_TEMPLATE | app.state.RAG_TEMPLATE = RAG_TEMPLATE | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL | app.state.PDF_EXTRACT_IMAGES = False | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| app.state.TOP_K = 4 |  | ||||||
| 
 |  | ||||||
| app.state.sentence_transformer_ef = ( | app.state.sentence_transformer_ef = ( | ||||||
|     embedding_functions.SentenceTransformerEmbeddingFunction( |     embedding_functions.SentenceTransformerEmbeddingFunction( | ||||||
|         model_name=get_embedding_model_path( |         model_name=get_embedding_model_path( | ||||||
|  | @ -121,6 +133,7 @@ async def get_status(): | ||||||
|         "chunk_size": app.state.CHUNK_SIZE, |         "chunk_size": app.state.CHUNK_SIZE, | ||||||
|         "chunk_overlap": app.state.CHUNK_OVERLAP, |         "chunk_overlap": app.state.CHUNK_OVERLAP, | ||||||
|         "template": app.state.RAG_TEMPLATE, |         "template": app.state.RAG_TEMPLATE, | ||||||
|  |         "embedding_engine": app.state.RAG_EMBEDDING_ENGINE, | ||||||
|         "embedding_model": app.state.RAG_EMBEDDING_MODEL, |         "embedding_model": app.state.RAG_EMBEDDING_MODEL, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -252,6 +265,17 @@ def query_doc_handler( | ||||||
| ): | ): | ||||||
| 
 | 
 | ||||||
|     try: |     try: | ||||||
|  |         if app.state.RAG_EMBEDDING_ENGINE == "ollama": | ||||||
|  |             query_embeddings = generate_ollama_embeddings( | ||||||
|  |                 {"model": app.state.RAG_EMBEDDING_MODEL, "prompt": form_data.query} | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             return query_embeddings_doc( | ||||||
|  |                 collection_name=form_data.collection_name, | ||||||
|  |                 query_embeddings=query_embeddings, | ||||||
|  |                 k=form_data.k if form_data.k else app.state.TOP_K, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|             return query_doc( |             return query_doc( | ||||||
|                 collection_name=form_data.collection_name, |                 collection_name=form_data.collection_name, | ||||||
|                 query=form_data.query, |                 query=form_data.query, | ||||||
|  | @ -277,12 +301,30 @@ def query_collection_handler( | ||||||
|     form_data: QueryCollectionsForm, |     form_data: QueryCollectionsForm, | ||||||
|     user=Depends(get_current_user), |     user=Depends(get_current_user), | ||||||
| ): | ): | ||||||
|  |     try: | ||||||
|  |         if app.state.RAG_EMBEDDING_ENGINE == "ollama": | ||||||
|  |             query_embeddings = generate_ollama_embeddings( | ||||||
|  |                 {"model": app.state.RAG_EMBEDDING_MODEL, "prompt": form_data.query} | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             return query_embeddings_collection( | ||||||
|  |                 collection_names=form_data.collection_names, | ||||||
|  |                 query_embeddings=query_embeddings, | ||||||
|  |                 k=form_data.k if form_data.k else app.state.TOP_K, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|             return query_collection( |             return query_collection( | ||||||
|                 collection_names=form_data.collection_names, |                 collection_names=form_data.collection_names, | ||||||
|                 query=form_data.query, |                 query=form_data.query, | ||||||
|                 k=form_data.k if form_data.k else app.state.TOP_K, |                 k=form_data.k if form_data.k else app.state.TOP_K, | ||||||
|                 embedding_function=app.state.sentence_transformer_ef, |                 embedding_function=app.state.sentence_transformer_ef, | ||||||
|             ) |             ) | ||||||
|  |     except Exception as e: | ||||||
|  |         log.exception(e) | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_400_BAD_REQUEST, | ||||||
|  |             detail=ERROR_MESSAGES.DEFAULT(e), | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.post("/web") | @app.post("/web") | ||||||
|  | @ -317,6 +359,7 @@ def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> b | ||||||
|         chunk_overlap=app.state.CHUNK_OVERLAP, |         chunk_overlap=app.state.CHUNK_OVERLAP, | ||||||
|         add_start_index=True, |         add_start_index=True, | ||||||
|     ) |     ) | ||||||
|  | 
 | ||||||
|     docs = text_splitter.split_documents(data) |     docs = text_splitter.split_documents(data) | ||||||
| 
 | 
 | ||||||
|     if len(docs) > 0: |     if len(docs) > 0: | ||||||
|  | @ -337,7 +380,9 @@ def store_text_in_vector_db( | ||||||
|     return store_docs_in_vector_db(docs, collection_name, overwrite) |     return store_docs_in_vector_db(docs, collection_name, overwrite) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool: | async def store_docs_in_vector_db( | ||||||
|  |     docs, collection_name, overwrite: bool = False | ||||||
|  | ) -> bool: | ||||||
| 
 | 
 | ||||||
|     texts = [doc.page_content for doc in docs] |     texts = [doc.page_content for doc in docs] | ||||||
|     metadatas = [doc.metadata for doc in docs] |     metadatas = [doc.metadata for doc in docs] | ||||||
|  | @ -349,6 +394,22 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b | ||||||
|                     log.info(f"deleting existing collection {collection_name}") |                     log.info(f"deleting existing collection {collection_name}") | ||||||
|                     CHROMA_CLIENT.delete_collection(name=collection_name) |                     CHROMA_CLIENT.delete_collection(name=collection_name) | ||||||
| 
 | 
 | ||||||
|  |         if app.state.RAG_EMBEDDING_ENGINE == "ollama": | ||||||
|  |             collection = CHROMA_CLIENT.create_collection(name=collection_name) | ||||||
|  | 
 | ||||||
|  |             for batch in create_batches( | ||||||
|  |                 api=CHROMA_CLIENT, | ||||||
|  |                 ids=[str(uuid.uuid1()) for _ in texts], | ||||||
|  |                 metadatas=metadatas, | ||||||
|  |                 embeddings=[ | ||||||
|  |                     generate_ollama_embeddings( | ||||||
|  |                         {"model": RAG_EMBEDDING_MODEL, "prompt": text} | ||||||
|  |                     ) | ||||||
|  |                     for text in texts | ||||||
|  |                 ], | ||||||
|  |             ): | ||||||
|  |                 collection.add(*batch) | ||||||
|  |         else: | ||||||
|             collection = CHROMA_CLIENT.create_collection( |             collection = CHROMA_CLIENT.create_collection( | ||||||
|                 name=collection_name, |                 name=collection_name, | ||||||
|                 embedding_function=app.state.sentence_transformer_ef, |                 embedding_function=app.state.sentence_transformer_ef, | ||||||
|  |  | ||||||
|  | @ -2,6 +2,9 @@ import os | ||||||
| import re | import re | ||||||
| import logging | import logging | ||||||
| from typing import List | from typing import List | ||||||
|  | import requests | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| from huggingface_hub import snapshot_download | from huggingface_hub import snapshot_download | ||||||
| 
 | 
 | ||||||
| from config import SRC_LOG_LEVELS, CHROMA_CLIENT | from config import SRC_LOG_LEVELS, CHROMA_CLIENT | ||||||
|  | @ -26,6 +29,21 @@ def query_doc(collection_name: str, query: str, k: int, embedding_function): | ||||||
|         raise e |         raise e | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def query_embeddings_doc(collection_name: str, query_embeddings, k: int): | ||||||
|  |     try: | ||||||
|  |         # if you use docker use the model from the environment variable | ||||||
|  |         collection = CHROMA_CLIENT.get_collection( | ||||||
|  |             name=collection_name, | ||||||
|  |         ) | ||||||
|  |         result = collection.query( | ||||||
|  |             query_embeddings=[query_embeddings], | ||||||
|  |             n_results=k, | ||||||
|  |         ) | ||||||
|  |         return result | ||||||
|  |     except Exception as e: | ||||||
|  |         raise e | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def merge_and_sort_query_results(query_results, k): | def merge_and_sort_query_results(query_results, k): | ||||||
|     # Initialize lists to store combined data |     # Initialize lists to store combined data | ||||||
|     combined_ids = [] |     combined_ids = [] | ||||||
|  | @ -96,6 +114,24 @@ def query_collection( | ||||||
|     return merge_and_sort_query_results(results, k) |     return merge_and_sort_query_results(results, k) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def query_embeddings_collection(collection_names: List[str], query_embeddings, k: int): | ||||||
|  | 
 | ||||||
|  |     results = [] | ||||||
|  |     for collection_name in collection_names: | ||||||
|  |         try: | ||||||
|  |             collection = CHROMA_CLIENT.get_collection(name=collection_name) | ||||||
|  | 
 | ||||||
|  |             result = collection.query( | ||||||
|  |                 query_embeddings=[query_embeddings], | ||||||
|  |                 n_results=k, | ||||||
|  |             ) | ||||||
|  |             results.append(result) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |     return merge_and_sort_query_results(results, k) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def rag_template(template: str, context: str, query: str): | def rag_template(template: str, context: str, query: str): | ||||||
|     template = template.replace("[context]", context) |     template = template.replace("[context]", context) | ||||||
|     template = template.replace("[query]", query) |     template = template.replace("[query]", query) | ||||||
|  |  | ||||||
|  | @ -405,6 +405,9 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "": | ||||||
| 
 | 
 | ||||||
| CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" | CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" | ||||||
| # this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (all-MiniLM-L6-v2) | # this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (all-MiniLM-L6-v2) | ||||||
|  | 
 | ||||||
|  | RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "") | ||||||
|  | 
 | ||||||
| RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2") | RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2") | ||||||
| log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"), | log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"), | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -220,6 +220,32 @@ export const generatePrompt = async (token: string = '', model: string, conversa | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export const generateEmbeddings = async (token: string = '', model: string, text: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/api/embeddings`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			Authorization: `Bearer ${token}` | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify({ | ||||||
|  | 			model: model, | ||||||
|  | 			prompt: text | ||||||
|  | 		}) | ||||||
|  | 	}).catch((err) => { | ||||||
|  | 		error = err; | ||||||
|  | 		return null; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export const generateTextCompletion = async (token: string = '', model: string, text: string) => { | export const generateTextCompletion = async (token: string = '', model: string, text: string) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ | ||||||
| 
 | 
 | ||||||
| 	let showResetConfirm = false; | 	let showResetConfirm = false; | ||||||
| 
 | 
 | ||||||
|  | 	let embeddingEngine = ''; | ||||||
| 	let chunkSize = 0; | 	let chunkSize = 0; | ||||||
| 	let chunkOverlap = 0; | 	let chunkOverlap = 0; | ||||||
| 	let pdfExtractImages = true; | 	let pdfExtractImages = true; | ||||||
|  | @ -119,58 +120,25 @@ | ||||||
| 			<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div> | 			<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div> | ||||||
| 
 | 
 | ||||||
| 			<div class=" flex w-full justify-between"> | 			<div class=" flex w-full justify-between"> | ||||||
| 				<div class=" self-center text-xs font-medium"> | 				<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Engine')}</div> | ||||||
| 					{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })} | 				<div class="flex items-center relative"> | ||||||
| 				</div> | 					<select | ||||||
| 
 | 						class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" | ||||||
| 				<button | 						bind:value={embeddingEngine} | ||||||
| 					class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading | 						placeholder="Select an embedding engine" | ||||||
| 						? ' cursor-not-allowed' |  | ||||||
| 						: ''}" |  | ||||||
| 					on:click={() => { |  | ||||||
| 						scanHandler(); |  | ||||||
| 						console.log('check'); |  | ||||||
| 					}} |  | ||||||
| 					type="button" |  | ||||||
| 					disabled={scanDirLoading} |  | ||||||
| 					> | 					> | ||||||
| 					<div class="self-center font-medium">{$i18n.t('Scan')}</div> | 						<option value="">{$i18n.t('Default (SentenceTransformer)')}</option> | ||||||
| 
 | 						<option value="ollama">{$i18n.t('Ollama')}</option> | ||||||
| 					{#if scanDirLoading} | 					</select> | ||||||
| 						<div class="ml-3 self-center"> | 				</div> | ||||||
| 							<svg |  | ||||||
| 								class=" w-3 h-3" |  | ||||||
| 								viewBox="0 0 24 24" |  | ||||||
| 								fill="currentColor" |  | ||||||
| 								xmlns="http://www.w3.org/2000/svg" |  | ||||||
| 								><style> |  | ||||||
| 									.spinner_ajPY { |  | ||||||
| 										transform-origin: center; |  | ||||||
| 										animation: spinner_AtaB 0.75s infinite linear; |  | ||||||
| 									} |  | ||||||
| 									@keyframes spinner_AtaB { |  | ||||||
| 										100% { |  | ||||||
| 											transform: rotate(360deg); |  | ||||||
| 										} |  | ||||||
| 									} |  | ||||||
| 								</style><path |  | ||||||
| 									d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" |  | ||||||
| 									opacity=".25" |  | ||||||
| 								/><path |  | ||||||
| 									d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" |  | ||||||
| 									class="spinner_ajPY" |  | ||||||
| 								/></svg |  | ||||||
| 							> |  | ||||||
| 						</div> |  | ||||||
| 					{/if} |  | ||||||
| 				</button> |  | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 
 |  | ||||||
| 		<hr class=" dark:border-gray-700" /> |  | ||||||
| 
 | 
 | ||||||
| 		<div class="space-y-2"> | 		<div class="space-y-2"> | ||||||
| 			<div> | 			<div> | ||||||
|  | 				{#if embeddingEngine === 'ollama'} | ||||||
|  | 					<div>da</div> | ||||||
|  | 				{:else} | ||||||
| 					<div class=" mb-2 text-sm font-medium">{$i18n.t('Update Embedding Model')}</div> | 					<div class=" mb-2 text-sm font-medium">{$i18n.t('Update Embedding Model')}</div> | ||||||
| 					<div class="flex w-full"> | 					<div class="flex w-full"> | ||||||
| 						<div class="flex-1 mr-2"> | 						<div class="flex-1 mr-2"> | ||||||
|  | @ -238,6 +206,57 @@ | ||||||
| 							'Warning: If you update or change your embedding model, you will need to re-import all documents.' | 							'Warning: If you update or change your embedding model, you will need to re-import all documents.' | ||||||
| 						)} | 						)} | ||||||
| 					</div> | 					</div> | ||||||
|  | 				{/if} | ||||||
|  | 
 | ||||||
|  | 				<hr class=" dark:border-gray-700 my-3" /> | ||||||
|  | 
 | ||||||
|  | 				<div class="  flex w-full justify-between"> | ||||||
|  | 					<div class=" self-center text-xs font-medium"> | ||||||
|  | 						{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })} | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<button | ||||||
|  | 						class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading | ||||||
|  | 							? ' cursor-not-allowed' | ||||||
|  | 							: ''}" | ||||||
|  | 						on:click={() => { | ||||||
|  | 							scanHandler(); | ||||||
|  | 							console.log('check'); | ||||||
|  | 						}} | ||||||
|  | 						type="button" | ||||||
|  | 						disabled={scanDirLoading} | ||||||
|  | 					> | ||||||
|  | 						<div class="self-center font-medium">{$i18n.t('Scan')}</div> | ||||||
|  | 
 | ||||||
|  | 						{#if scanDirLoading} | ||||||
|  | 							<div class="ml-3 self-center"> | ||||||
|  | 								<svg | ||||||
|  | 									class=" w-3 h-3" | ||||||
|  | 									viewBox="0 0 24 24" | ||||||
|  | 									fill="currentColor" | ||||||
|  | 									xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 									><style> | ||||||
|  | 										.spinner_ajPY { | ||||||
|  | 											transform-origin: center; | ||||||
|  | 											animation: spinner_AtaB 0.75s infinite linear; | ||||||
|  | 										} | ||||||
|  | 										@keyframes spinner_AtaB { | ||||||
|  | 											100% { | ||||||
|  | 												transform: rotate(360deg); | ||||||
|  | 											} | ||||||
|  | 										} | ||||||
|  | 									</style><path | ||||||
|  | 										d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||||
|  | 										opacity=".25" | ||||||
|  | 									/><path | ||||||
|  | 										d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||||
|  | 										class="spinner_ajPY" | ||||||
|  | 									/></svg | ||||||
|  | 								> | ||||||
|  | 							</div> | ||||||
|  | 						{/if} | ||||||
|  | 					</button> | ||||||
|  | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<hr class=" dark:border-gray-700 my-3" /> | 				<hr class=" dark:border-gray-700 my-3" /> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek