Timothy J. Baek 8 月之前
父节点
当前提交
6a21a77ee9

+ 11 - 1
backend/apps/rag/main.py

@@ -434,6 +434,11 @@ async def get_rag_config(user=Depends(get_admin_user)):
     }
 
 
+class FileConfig(BaseModel):
+    max_size: Optional[int] = None
+    max_count: Optional[int] = None
+
+
 class ContentExtractionConfig(BaseModel):
     engine: str = ""
     tika_server_url: Optional[str] = None
@@ -472,6 +477,7 @@ class WebConfig(BaseModel):
 
 class ConfigUpdateForm(BaseModel):
     pdf_extract_images: Optional[bool] = None
+    file: Optional[FileConfig] = None
     content_extraction: Optional[ContentExtractionConfig] = None
     chunk: Optional[ChunkParamUpdateForm] = None
     youtube: Optional[YoutubeLoaderConfig] = None
@@ -486,6 +492,10 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
         else app.state.config.PDF_EXTRACT_IMAGES
     )
 
+    if form_data.file is not None:
+        app.state.config.FILE_MAX_SIZE = form_data.file.max_size
+        app.state.config.FILE_MAX_COUNT = form_data.file.max_count
+
     if form_data.content_extraction is not None:
         log.info(f"Updating text settings: {form_data.content_extraction}")
         app.state.config.CONTENT_EXTRACTION_ENGINE = form_data.content_extraction.engine
@@ -526,11 +536,11 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
 
     return {
         "status": True,
+        "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
         "file": {
             "max_size": app.state.config.FILE_MAX_SIZE,
             "max_count": app.state.config.FILE_MAX_COUNT,
         },
-        "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
         "content_extraction": {
             "engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
             "tika_server_url": app.state.config.TIKA_SERVER_URL,

+ 4 - 0
backend/main.py

@@ -1939,6 +1939,10 @@ async def get_app_config(request: Request):
                         "engine": audio_app.state.config.STT_ENGINE,
                     },
                 },
+                "file": {
+                    "max_size": rag_app.state.config.FILE_MAX_SIZE,
+                    "max_count": rag_app.state.config.FILE_MAX_COUNT,
+                },
                 "permissions": {**webui_app.state.config.USER_PERMISSIONS},
             }
             if user is not None

+ 4 - 1
src/lib/components/admin/Settings.svelte

@@ -359,8 +359,11 @@
 			<Models />
 		{:else if selectedTab === 'documents'}
 			<Documents
-				saveHandler={() => {
+				on:save={async () => {
 					toast.success($i18n.t('Settings saved successfully!'));
+
+					await tick();
+					await config.set(await getBackendConfig());
 				}}
 			/>
 		{:else if selectedTab === 'web'}

+ 52 - 32
src/lib/components/admin/Settings/Documents.svelte

@@ -1,4 +1,8 @@
 <script lang="ts">
+	import { onMount, getContext, createEventDispatcher } from 'svelte';
+
+	const dispatch = createEventDispatcher();
+
 	import { getDocs } from '$lib/apis/documents';
 	import { deleteAllFiles, deleteFileById } from '$lib/apis/files';
 	import {
@@ -18,14 +22,12 @@
 	import ResetVectorDBConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 
 	import { documents, models } from '$lib/stores';
-	import { onMount, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 	const i18n = getContext('i18n');
 
-	export let saveHandler: Function;
-
 	let scanDirLoading = false;
 	let updateEmbeddingModelLoading = false;
 	let updateRerankingModelLoading = false;
@@ -164,19 +166,22 @@
 	};
 
 	const submitHandler = async () => {
-		embeddingModelUpdateHandler();
+		await embeddingModelUpdateHandler();
 
 		if (querySettings.hybrid) {
-			rerankingModelUpdateHandler();
+			await rerankingModelUpdateHandler();
 		}
 
 		if (contentExtractionEngine === 'tika' && tikaServerUrl === '') {
 			toast.error($i18n.t('Tika Server URL required.'));
 			return;
 		}
-
 		const res = await updateRAGConfig(localStorage.token, {
 			pdf_extract_images: pdfExtractImages,
+			file: {
+				max_size: fileMaxSize === '' ? null : fileMaxSize,
+				max_count: fileMaxCount === '' ? null : fileMaxCount
+			},
 			chunk: {
 				chunk_overlap: chunkOverlap,
 				chunk_size: chunkSize
@@ -188,6 +193,8 @@
 		});
 
 		await updateQuerySettings(localStorage.token, querySettings);
+
+		dispatch('save');
 	};
 
 	const setEmbeddingConfig = async () => {
@@ -233,8 +240,8 @@
 			tikaServerUrl = res.content_extraction.tika_server_url;
 			showTikaServerUrl = contentExtractionEngine === 'tika';
 
-			fileMaxSize = res.file.max_size;
-			fileMaxCount = res.file.max_count;
+			fileMaxSize = res?.file.max_size ?? '';
+			fileMaxCount = res?.file.max_count ?? '';
 		}
 	});
 </script>
@@ -271,7 +278,6 @@
 	class="flex flex-col h-full justify-between space-y-3 text-sm"
 	on:submit|preventDefault={() => {
 		submitHandler();
-		saveHandler();
 	}}
 >
 	<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5">
@@ -622,36 +628,50 @@
 			<div class="text-sm font-medium">{$i18n.t('Files')}</div>
 
 			<div class=" my-2 flex gap-1.5">
-				<div class="  w-full justify-between">
-					<div class="self-center text-xs font-medium min-w-fit mb-1">
-						{$i18n.t('Max Upload Count')}
+				<div class="w-full">
+					<div class=" self-center text-xs font-medium min-w-fit mb-1">
+						{$i18n.t('Max Upload Size')}
 					</div>
+
 					<div class="self-center">
-						<input
-							class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-							type="number"
-							placeholder={$i18n.t('Enter File Count')}
-							bind:value={fileMaxCount}
-							autocomplete="off"
-							min="0"
-						/>
+						<Tooltip
+							content={$i18n.t(
+								'The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.'
+							)}
+							placement="top-start"
+						>
+							<input
+								class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								type="number"
+								placeholder={$i18n.t('Leave empty for unlimited')}
+								bind:value={fileMaxSize}
+								autocomplete="off"
+								min="0"
+							/>
+						</Tooltip>
 					</div>
 				</div>
 
-				<div class="w-full">
-					<div class=" self-center text-xs font-medium min-w-fit mb-1">
-						{$i18n.t('Max Upload Size')}
+				<div class="  w-full">
+					<div class="self-center text-xs font-medium min-w-fit mb-1">
+						{$i18n.t('Max Upload Count')}
 					</div>
-
 					<div class="self-center">
-						<input
-							class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-							type="number"
-							placeholder={$i18n.t('Enter File Size (MB)')}
-							bind:value={fileMaxSize}
-							autocomplete="off"
-							min="0"
-						/>
+						<Tooltip
+							content={$i18n.t(
+								'The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.'
+							)}
+							placement="top-start"
+						>
+							<input
+								class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								type="number"
+								placeholder={$i18n.t('Leave empty for unlimited')}
+								bind:value={fileMaxCount}
+								autocomplete="off"
+								min="0"
+							/>
+						</Tooltip>
 					</div>
 				</div>
 			</div>

+ 21 - 0
src/lib/components/chat/Chat.svelte

@@ -542,6 +542,27 @@
 					`Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.`
 				)
 			);
+		} else if (
+			($config?.file?.max_count ?? null) !== null &&
+			files.length + chatFiles.length > $config?.file?.max_count
+		) {
+			console.log(chatFiles.length, files.length);
+			toast.error(
+				$i18n.t(`You can only chat with a maximum of {{maxCount}} file(s) at a time.`, {
+					maxCount: $config?.file?.max_count
+				})
+			);
+		} else if (
+			($config?.file?.max_size ?? null) !== null &&
+			[...files, ...chatFiles].some(
+				(file) => file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
+			)
+		) {
+			toast.error(
+				$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
+					maxSize: $config?.file?.max_size
+				})
+			);
 		} else {
 			// Reset chat input textarea
 			const chatTextAreaElement = document.getElementById('chat-textarea');

+ 14 - 1
src/lib/components/chat/MessageInput.svelte

@@ -173,6 +173,20 @@
 
 	const inputFilesHandler = async (inputFiles) => {
 		inputFiles.forEach((file) => {
+			console.log(file, file.name.split('.').at(-1));
+
+			if (
+				($config?.file?.max_size ?? null) !== null &&
+				file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
+			) {
+				toast.error(
+					$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
+						maxSize: $config?.file?.max_size
+					})
+				);
+				return;
+			}
+
 			if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
 				if (visionCapableModels.length === 0) {
 					toast.error($i18n.t('Selected model(s) do not support image inputs'));
@@ -222,7 +236,6 @@
 
 			if (e.dataTransfer?.files) {
 				const inputFiles = Array.from(e.dataTransfer?.files);
-				console.log(file, file.name.split('.').at(-1));
 				if (inputFiles && inputFiles.length > 0) {
 					console.log(inputFiles);
 					inputFilesHandler(inputFiles);