Files
open-webui/src/lib/components/admin/Settings/Documents.svelte
Timothy Jaeryang Baek ecad20b77f refac
2026-08-24 17:17:53 -04:00

1590 lines
52 KiB
Svelte

<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import {
getQuerySettings,
updateQuerySettings,
resetVectorDB,
getEmbeddingConfig,
updateEmbeddingConfig,
getRerankingConfig,
updateRerankingConfig,
getRAGConfig,
updateRAGConfig
} from '$lib/apis/retrieval';
import { reindexKnowledgeFiles, reindexKnowledgeMetadata } from '$lib/apis/knowledge';
import { reindexMemoryVectors } from '$lib/apis/memories';
import { deleteAllFiles } from '$lib/apis/files';
import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import ResetVectorDBConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import ReindexEmbeddingDataConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import SettingsSelect from '$lib/components/common/SettingsSelect.svelte';
import AdminSettingField from './AdminSettingField.svelte';
import AdminSettingRow from './AdminSettingRow.svelte';
import AdminSettingSection from './AdminSettingSection.svelte';
const i18n: any = getContext('i18n');
let updateEmbeddingModelLoading = false;
let updateRerankingModelLoading = false;
let showResetConfirm = false;
let showResetUploadDirConfirm = false;
let showReindexConfirm = false;
let showExternalDocumentLoaderHeadersHint = false;
let RAG_EMBEDDING_ENGINE = '';
let RAG_EMBEDDING_MODEL = '';
let RAG_EMBEDDING_BATCH_SIZE = 1;
let ENABLE_ASYNC_EMBEDDING = true;
let RAG_EMBEDDING_CONCURRENT_REQUESTS = 0;
let rerankingModel = '';
let OpenAIUrl = '';
let OpenAIKey = '';
let AzureOpenAIUrl = '';
let AzureOpenAIKey = '';
let AzureOpenAIVersion = '';
let OllamaUrl = '';
let OllamaKey = '';
let querySettings = {
template: '',
r: 0.0,
k: 4,
k_reranker: 4,
hybrid: false
};
let RAGConfig: any = null;
const inputClass =
'w-full h-7 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const actionButtonClass =
'shrink-0 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
const textareaClass =
'w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
const embeddingModelUpdateHandler = async () => {
if (RAG_EMBEDDING_ENGINE === '' && RAG_EMBEDDING_MODEL.split('/').length - 1 > 1) {
toast.error(
$i18n.t(
'Model filesystem path detected. Model shortname is required for update, cannot continue.'
)
);
return;
}
if (RAG_EMBEDDING_ENGINE === 'ollama' && RAG_EMBEDDING_MODEL === '') {
toast.error(
$i18n.t(
'Model filesystem path detected. Model shortname is required for update, cannot continue.'
)
);
return;
}
if (RAG_EMBEDDING_ENGINE === 'openai' && RAG_EMBEDDING_MODEL === '') {
toast.error(
$i18n.t(
'Model filesystem path detected. Model shortname is required for update, cannot continue.'
)
);
return;
}
if (
RAG_EMBEDDING_ENGINE === 'azure_openai' &&
(AzureOpenAIKey === '' || AzureOpenAIUrl === '' || AzureOpenAIVersion === '')
) {
toast.error($i18n.t('OpenAI URL/Key required.'));
return;
}
console.debug('Update embedding model attempt:', {
RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_BATCH_SIZE,
ENABLE_ASYNC_EMBEDDING,
RAG_EMBEDDING_CONCURRENT_REQUESTS
});
updateEmbeddingModelLoading = true;
const payload: Parameters<typeof updateEmbeddingConfig>[1] = {
RAG_EMBEDDING_ENGINE: RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_MODEL: RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_BATCH_SIZE: RAG_EMBEDDING_BATCH_SIZE,
ENABLE_ASYNC_EMBEDDING: ENABLE_ASYNC_EMBEDDING,
RAG_EMBEDDING_CONCURRENT_REQUESTS: RAG_EMBEDDING_CONCURRENT_REQUESTS
};
if (RAG_EMBEDDING_ENGINE === 'ollama') {
payload.ollama_config = {
key: OllamaKey,
url: OllamaUrl
};
} else if (RAG_EMBEDDING_ENGINE === 'openai') {
payload.openai_config = {
key: OpenAIKey,
url: OpenAIUrl
};
} else if (RAG_EMBEDDING_ENGINE === 'azure_openai') {
payload.azure_openai_config = {
key: AzureOpenAIKey,
url: AzureOpenAIUrl,
version: AzureOpenAIVersion
};
}
const res = await updateEmbeddingConfig(localStorage.token, payload).catch(async (error) => {
toast.error(`${error}`);
await setEmbeddingConfig();
return null;
});
updateEmbeddingModelLoading = false;
if (res) {
console.debug('embeddingModelUpdateHandler:', res);
}
};
const submitHandler = async () => {
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'external' &&
RAGConfig.EXTERNAL_DOCUMENT_LOADER_URL === ''
) {
toast.error($i18n.t('External Document Loader URL required.'));
return;
}
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'external' &&
RAGConfig.EXTERNAL_DOCUMENT_LOADER_HEADERS
) {
try {
const headers = JSON.parse(RAGConfig.EXTERNAL_DOCUMENT_LOADER_HEADERS);
if (headers === null || typeof headers !== 'object' || Array.isArray(headers)) {
throw new Error('Headers must be a valid JSON object');
}
RAGConfig.EXTERNAL_DOCUMENT_LOADER_HEADERS = JSON.stringify(headers, null, 2);
} catch (error) {
toast.error($i18n.t('Headers must be a valid JSON object'));
return;
}
}
if (RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika' && RAGConfig.TIKA_SERVER_URL === '') {
toast.error($i18n.t('Tika Server URL required.'));
return;
}
if (RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling' && RAGConfig.DOCLING_SERVER_URL === '') {
toast.error($i18n.t('Docling Server URL required.'));
return;
}
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'datalab_marker' &&
RAGConfig.DATALAB_MARKER_ADDITIONAL_CONFIG &&
RAGConfig.DATALAB_MARKER_ADDITIONAL_CONFIG.trim() !== ''
) {
try {
JSON.parse(RAGConfig.DATALAB_MARKER_ADDITIONAL_CONFIG);
} catch (e) {
toast.error($i18n.t('Invalid JSON format in Additional Config'));
return;
}
}
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence' &&
RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT === ''
) {
toast.error($i18n.t('Document Intelligence endpoint required.'));
return;
}
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr' &&
RAGConfig.MISTRAL_OCR_API_KEY === ''
) {
toast.error($i18n.t('Mistral OCR API Key required.'));
return;
}
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'paddleocr_vl' &&
RAGConfig.PADDLEOCR_VL_BASE_URL === ''
) {
toast.error($i18n.t('PaddleOCR-vl API URL required.'));
return;
}
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mineru' &&
RAGConfig.MINERU_API_MODE === 'cloud' &&
RAGConfig.MINERU_API_KEY === ''
) {
toast.error($i18n.t('MinerU API Key required for Cloud API mode.'));
return;
}
if (!RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL) {
await embeddingModelUpdateHandler();
}
if (RAGConfig.DOCLING_PARAMS) {
try {
JSON.parse(RAGConfig.DOCLING_PARAMS);
} catch (e) {
toast.error(
$i18n.t('Invalid JSON format in {{NAME}}', {
NAME: $i18n.t('Docling Parameters')
})
);
return;
}
}
if (RAGConfig.MINERU_PARAMS) {
try {
JSON.parse(RAGConfig.MINERU_PARAMS);
} catch (e) {
toast.error($i18n.t('Invalid JSON format in MinerU Parameters'));
return;
}
}
const res = await updateRAGConfig(localStorage.token, {
...RAGConfig,
// Convert null (from cleared number inputs) to empty string so the backend
// can distinguish "clear this field" from "don't change this field"
FILE_MAX_SIZE: RAGConfig.FILE_MAX_SIZE ?? '',
FILE_MAX_COUNT: RAGConfig.FILE_MAX_COUNT ?? '',
FILE_IMAGE_COMPRESSION_WIDTH: RAGConfig.FILE_IMAGE_COMPRESSION_WIDTH ?? '',
FILE_IMAGE_COMPRESSION_HEIGHT: RAGConfig.FILE_IMAGE_COMPRESSION_HEIGHT ?? '',
ALLOWED_FILE_EXTENSIONS: RAGConfig.ALLOWED_FILE_EXTENSIONS.split(',')
.map((ext) => ext.trim())
.filter((ext) => ext !== ''),
DOCLING_PARAMS:
typeof RAGConfig.DOCLING_PARAMS === 'string' && RAGConfig.DOCLING_PARAMS.trim() !== ''
? JSON.parse(RAGConfig.DOCLING_PARAMS)
: {},
EXTERNAL_DOCUMENT_LOADER_HEADERS:
typeof RAGConfig.EXTERNAL_DOCUMENT_LOADER_HEADERS === 'string' &&
RAGConfig.EXTERNAL_DOCUMENT_LOADER_HEADERS.trim() !== ''
? JSON.parse(RAGConfig.EXTERNAL_DOCUMENT_LOADER_HEADERS)
: {},
CONTENT_EXTRACTION_SUPPORTED_MEDIA_MIME_TYPES:
RAGConfig.CONTENT_EXTRACTION_SUPPORTED_MEDIA_MIME_TYPES === null
? undefined
: RAGConfig.CONTENT_EXTRACTION_SUPPORTED_MEDIA_MIME_TYPES.split(',')
.map((mimeType: string) => mimeType.trim())
.filter((mimeType: string) => mimeType !== ''),
MINERU_PARAMS:
typeof RAGConfig.MINERU_PARAMS === 'string' && RAGConfig.MINERU_PARAMS.trim() !== ''
? JSON.parse(RAGConfig.MINERU_PARAMS)
: {},
MINERU_FILE_EXTENSIONS: RAGConfig.MINERU_FILE_EXTENSIONS.split(',')
.map((ext) => ext.trim())
.filter((ext) => ext !== '')
});
dispatch('save');
};
const setEmbeddingConfig = async () => {
const embeddingConfig = await getEmbeddingConfig(localStorage.token);
if (embeddingConfig) {
RAG_EMBEDDING_ENGINE = embeddingConfig.RAG_EMBEDDING_ENGINE;
RAG_EMBEDDING_MODEL = embeddingConfig.RAG_EMBEDDING_MODEL;
RAG_EMBEDDING_BATCH_SIZE = embeddingConfig.RAG_EMBEDDING_BATCH_SIZE ?? 1;
ENABLE_ASYNC_EMBEDDING = embeddingConfig.ENABLE_ASYNC_EMBEDDING ?? true;
RAG_EMBEDDING_CONCURRENT_REQUESTS = embeddingConfig.RAG_EMBEDDING_CONCURRENT_REQUESTS ?? 0;
OpenAIKey = embeddingConfig.openai_config.key ?? '';
OpenAIUrl = embeddingConfig.openai_config.url ?? '';
OllamaKey = embeddingConfig.ollama_config.key ?? '';
OllamaUrl = embeddingConfig.ollama_config.url ?? '';
AzureOpenAIKey = embeddingConfig.azure_openai_config.key ?? '';
AzureOpenAIUrl = embeddingConfig.azure_openai_config.url ?? '';
AzureOpenAIVersion = embeddingConfig.azure_openai_config.version ?? '';
}
};
onMount(async () => {
await setEmbeddingConfig();
const config = await getRAGConfig(localStorage.token);
config.ALLOWED_FILE_EXTENSIONS = (config?.ALLOWED_FILE_EXTENSIONS ?? []).join(', ');
config.DOCLING_PARAMS =
typeof config.DOCLING_PARAMS === 'object'
? JSON.stringify(config.DOCLING_PARAMS ?? {}, null, 2)
: config.DOCLING_PARAMS;
config.MINERU_PARAMS =
typeof config.MINERU_PARAMS === 'object'
? JSON.stringify(config.MINERU_PARAMS ?? {}, null, 2)
: config.MINERU_PARAMS;
config.EXTERNAL_DOCUMENT_LOADER_HEADERS =
typeof config.EXTERNAL_DOCUMENT_LOADER_HEADERS === 'object'
? Object.keys(config.EXTERNAL_DOCUMENT_LOADER_HEADERS ?? {}).length > 0
? JSON.stringify(config.EXTERNAL_DOCUMENT_LOADER_HEADERS, null, 2)
: ''
: config.EXTERNAL_DOCUMENT_LOADER_HEADERS;
config.MINERU_FILE_EXTENSIONS = (config?.MINERU_FILE_EXTENSIONS ?? ['pdf']).join(', ');
config.CONTENT_EXTRACTION_SUPPORTED_MEDIA_MIME_TYPES =
config?.CONTENT_EXTRACTION_SUPPORTED_MEDIA_MIME_TYPES?.join(', ') ?? null;
config.RAG_TOKENIZER_MODEL = config?.RAG_TOKENIZER_MODEL ?? '';
RAGConfig = config;
});
</script>
<ResetUploadDirConfirmDialog
bind:show={showResetUploadDirConfirm}
on:confirm={async () => {
const res = await deleteAllFiles(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
toast.success($i18n.t('Success'));
}
}}
/>
<ResetVectorDBConfirmDialog
bind:show={showResetConfirm}
on:confirm={() => {
const res = resetVectorDB(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
toast.success($i18n.t('Success'));
}
}}
/>
<ReindexEmbeddingDataConfirmDialog
bind:show={showReindexConfirm}
title={$i18n.t('Reindex Embedding Data')}
message={$i18n.t(
'Rebuild knowledge file, knowledge search, and memory vectors using the current embedding model.'
)}
on:confirm={async () => {
const knowledgeRes = await reindexKnowledgeFiles(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (!knowledgeRes) {
return;
}
const knowledgeMetadataRes = await reindexKnowledgeMetadata(localStorage.token).catch(
(error) => {
toast.error(`${error}`);
return null;
}
);
if (!knowledgeMetadataRes) {
return;
}
const memoryRes = await reindexMemoryVectors(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (memoryRes) {
toast.success($i18n.t('Success'));
}
}}
/>
<form
class="flex h-full flex-col justify-between text-sm"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">{$i18n.t('Documents')}</h2>
{#if RAGConfig}
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<AdminSettingSection title={$i18n.t('Content Extraction')} first>
<AdminSettingRow
label={$i18n.t('Content Extraction Engine')}
description={$i18n.t('Choose how uploaded documents are parsed before indexing.')}
>
<SettingsSelect bind:value={RAGConfig.CONTENT_EXTRACTION_ENGINE}>
<option value="">{$i18n.t('Default')}</option>
<option value="external">{$i18n.t('External')}</option>
<option value="tika">{$i18n.t('Tika')}</option>
<option value="docling">{$i18n.t('Docling')}</option>
<option value="datalab_marker">{$i18n.t('Datalab Marker API')}</option>
<option value="document_intelligence">{$i18n.t('Document Intelligence')}</option>
<option value="mistral_ocr">{$i18n.t('Mistral OCR')}</option>
<option value="paddleocr_vl">{$i18n.t('PaddleOCR-vl')}</option>
<option value="mineru">{$i18n.t('MinerU')}</option>
</SettingsSelect>
</AdminSettingRow>
<AdminSettingField
label={$i18n.t('Supported Media MIME Types')}
description={$i18n.t(
'Media upload MIME types the content extraction engine may process.'
)}
>
<input
class={inputClass}
placeholder={$i18n.t('image/*, video/*')}
bind:value={RAGConfig.CONTENT_EXTRACTION_SUPPORTED_MEDIA_MIME_TYPES}
/>
</AdminSettingField>
{#if RAGConfig.CONTENT_EXTRACTION_ENGINE === ''}
<AdminSettingRow
label={$i18n.t('PDF Extract Images (OCR)')}
description={$i18n.t('Extract images from PDFs so OCR can process image-only pages.')}
let:labelId
>
<Switch bind:state={RAGConfig.PDF_EXTRACT_IMAGES} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('PDF Loader Mode')}
description={$i18n.t(
'Page mode creates one document per page. Single mode keeps pages together for chunking across boundaries.'
)}
>
<SettingsSelect bind:value={RAGConfig.PDF_LOADER_MODE}>
<option value="page">{$i18n.t('Page')}</option>
<option value="single">{$i18n.t('Single')}</option>
</SettingsSelect>
</AdminSettingRow>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'datalab_marker'}
<AdminSettingField
label={$i18n.t('API Base URL')}
description={$i18n.t('Datalab Marker service endpoint used for document parsing.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Enter Datalab Marker API Base URL')}
bind:value={RAGConfig.DATALAB_MARKER_API_BASE_URL}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Key')}
description={$i18n.t('API key used to authenticate with Datalab Marker.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('Enter Datalab Marker API Key')}
required={false}
bind:value={RAGConfig.DATALAB_MARKER_API_KEY}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('Additional Config')}
description={$i18n.t('JSON options passed to Marker for advanced parsing behavior.')}
>
<Tooltip
content={$i18n.t(
'Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, \'{"key": "value"}\'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level'
)}
placement="top-start"
className="w-full"
>
<Textarea
className={textareaClass}
bind:value={RAGConfig.DATALAB_MARKER_ADDITIONAL_CONFIG}
placeholder={$i18n.t('Enter JSON config (e.g., {"disable_links": true})')}
/>
</Tooltip>
</AdminSettingField>
<AdminSettingRow
label={$i18n.t('Use LLM')}
description={$i18n.t(
'Use an LLM to improve tables, forms, math, and layout detection.'
)}
let:labelId
>
<Switch bind:state={RAGConfig.DATALAB_MARKER_USE_LLM} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Skip Cache')}
description={$i18n.t('Skip cached Marker results and rerun inference.')}
let:labelId
>
<Switch bind:state={RAGConfig.DATALAB_MARKER_SKIP_CACHE} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Force OCR')}
description={$i18n.t('Run OCR on all PDF pages, even pages with embedded text.')}
let:labelId
>
<Switch bind:state={RAGConfig.DATALAB_MARKER_FORCE_OCR} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Paginate')}
description={$i18n.t('Separate output by page with page markers.')}
let:labelId
>
<Switch bind:state={RAGConfig.DATALAB_MARKER_PAGINATE} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Strip Existing OCR')}
description={$i18n.t('Remove existing OCR text and rerun OCR when Force OCR is off.')}
let:labelId
>
<Switch
bind:state={RAGConfig.DATALAB_MARKER_STRIP_EXISTING_OCR}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Disable Image Extraction')}
description={$i18n.t('Do not extract images from PDFs during Marker processing.')}
let:labelId
>
<Switch
bind:state={RAGConfig.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Format Lines')}
description={$i18n.t('Format lines to detect inline math and styles.')}
let:labelId
>
<Switch bind:state={RAGConfig.DATALAB_MARKER_FORMAT_LINES} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Output Format')}
description={$i18n.t('Text output format returned by Marker.')}
>
<SettingsSelect bind:value={RAGConfig.DATALAB_MARKER_OUTPUT_FORMAT}>
<option value="markdown">{$i18n.t('Markdown')}</option>
<option value="json">{$i18n.t('JSON')}</option>
<option value="html">{$i18n.t('HTML')}</option>
</SettingsSelect>
</AdminSettingRow>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'external'}
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('Document Loader URL')}
description={$i18n.t('External service endpoint used to load document content.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Enter External Document Loader URL')}
bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_URL}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Key')}
description={$i18n.t('API key sent to the external document loader.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('Enter External Document Loader API Key')}
required={false}
bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_API_KEY}
/>
</AdminSettingField>
</div>
<AdminSettingField
label={$i18n.t('Headers')}
description={$i18n.t('Additional JSON headers sent to the external document loader.')}
>
<Tooltip
content={$i18n.t(
'Enter additional headers in JSON format (e.g. {"X-Custom-Header": "value"}'
)}
>
<Textarea
className={textareaClass}
bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_HEADERS}
placeholder={$i18n.t('Enter additional headers in JSON format')}
required={false}
/>
</Tooltip>
<button
type="button"
class="mt-1 text-[0.6875rem] text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
on:click={() =>
(showExternalDocumentLoaderHeadersHint = !showExternalDocumentLoaderHeadersHint)}
>
{$i18n.t('Header variables')}
</button>
{#if showExternalDocumentLoaderHeadersHint}
<div class="mt-1 text-[0.6875rem] leading-5 text-gray-500 dark:text-gray-400">
<div>{$i18n.t('No additional headers are sent unless configured.')}</div>
<div>
{$i18n.t('Example')}:
<code class="text-gray-700 dark:text-gray-300"
>{'{"X-OpenWebUI-File-Id": "{{FILE_ID}}"}'}</code
>
</div>
<div>
{$i18n.t('Available variables')}:
<code class="text-gray-700 dark:text-gray-300">{'{{FILE_ID}}'}</code>,
<code class="text-gray-700 dark:text-gray-300">{'{{FILE_NAME}}'}</code>,
<code class="text-gray-700 dark:text-gray-300">{'{{FILE_CONTENT_TYPE}}'}</code>
</div>
</div>
{/if}
</AdminSettingField>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika'}
<AdminSettingField
label={$i18n.t('Tika Server URL')}
description={$i18n.t('Tika server endpoint used for content extraction.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Enter Tika Server URL')}
bind:value={RAGConfig.TIKA_SERVER_URL}
/>
</AdminSettingField>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling'}
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('Docling Server URL')}
description={$i18n.t('Docling service endpoint used for parsing.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Enter Docling Server URL')}
bind:value={RAGConfig.DOCLING_SERVER_URL}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Key')}
description={$i18n.t('API key sent to Docling.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('Enter Docling API Key')}
bind:value={RAGConfig.DOCLING_API_KEY}
required={false}
/>
</AdminSettingField>
</div>
<AdminSettingField
label={$i18n.t('Parameters')}
description={$i18n.t('Additional Docling parameters in JSON format.')}
>
<Textarea
className={textareaClass}
bind:value={RAGConfig.DOCLING_PARAMS}
placeholder={$i18n.t('Enter additional parameters in JSON format')}
minSize={100}
/>
</AdminSettingField>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('Endpoint')}
description={$i18n.t('Document Intelligence endpoint used for parsing.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Enter Document Intelligence Endpoint')}
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('Key')}
description={$i18n.t('Credential used for Document Intelligence.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('Enter Document Intelligence Key')}
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_KEY}
required={false}
/>
</AdminSettingField>
</div>
<AdminSettingField
label={$i18n.t('Document Intelligence Model')}
description={$i18n.t('Model name used by Document Intelligence.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Enter Document Intelligence Model')}
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_MODEL}
/>
</AdminSettingField>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'}
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('API Base URL')}
description={$i18n.t('Mistral OCR service endpoint.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Enter Mistral API Base URL')}
bind:value={RAGConfig.MISTRAL_OCR_API_BASE_URL}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Key')}
description={$i18n.t('API key sent to Mistral OCR.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('Enter Mistral API Key')}
bind:value={RAGConfig.MISTRAL_OCR_API_KEY}
/>
</AdminSettingField>
</div>
<AdminSettingRow
label={$i18n.t('Use Base64')}
description={$i18n.t('Send PDFs as base64 data URLs instead of uploading first.')}
let:labelId
>
<Switch bind:state={RAGConfig.MISTRAL_OCR_USE_BASE64} ariaLabelledbyId={labelId} />
</AdminSettingRow>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'paddleocr_vl'}
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('API Base URL')}
description={$i18n.t('PaddleOCR-vl service endpoint.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Enter PaddleOCR-vl API Base URL')}
bind:value={RAGConfig.PADDLEOCR_VL_BASE_URL}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Token')}
description={$i18n.t('API token sent to PaddleOCR-vl.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('Enter PaddleOCR-vl API Token')}
bind:value={RAGConfig.PADDLEOCR_VL_TOKEN}
required={false}
/>
</AdminSettingField>
</div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mineru'}
<AdminSettingRow
label={$i18n.t('API Mode')}
description={$i18n.t('Choose the local or cloud MinerU API mode.')}
>
<SettingsSelect
bind:value={RAGConfig.MINERU_API_MODE}
on:change={() => {
const cloudUrl = 'https://mineru.net/api/v4';
const localUrl = 'http://localhost:8000';
if (RAGConfig.MINERU_API_MODE === 'cloud') {
if (!RAGConfig.MINERU_API_URL || RAGConfig.MINERU_API_URL === localUrl) {
RAGConfig.MINERU_API_URL = cloudUrl;
}
} else {
if (!RAGConfig.MINERU_API_URL || RAGConfig.MINERU_API_URL === cloudUrl) {
RAGConfig.MINERU_API_URL = localUrl;
}
}
}}
>
<option value="local">{$i18n.t('local')}</option>
<option value="cloud">{$i18n.t('cloud')}</option>
</SettingsSelect>
</AdminSettingRow>
<AdminSettingField
label={$i18n.t('API URL')}
description={$i18n.t('MinerU API endpoint for the selected mode.')}
>
<input
class={inputClass}
placeholder={RAGConfig.MINERU_API_MODE === 'cloud'
? $i18n.t('https://mineru.net/api/v4')
: $i18n.t('http://localhost:8000')}
bind:value={RAGConfig.MINERU_API_URL}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Key')}
description={$i18n.t('API key used for MinerU cloud mode.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('Enter MinerU API Key')}
bind:value={RAGConfig.MINERU_API_KEY}
/>
</AdminSettingField>
<AdminSettingRow
label={$i18n.t('API Timeout')}
description={$i18n.t('Maximum time in seconds to wait for MinerU API responses.')}
>
<input
class="w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-right text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
type="number"
min="1"
bind:value={RAGConfig.MINERU_API_TIMEOUT}
placeholder="60"
/>
</AdminSettingRow>
<AdminSettingField
label={$i18n.t('Parameters')}
description={$i18n.t('Advanced MinerU parsing parameters in JSON format.')}
>
<Textarea
className={textareaClass}
bind:value={RAGConfig.MINERU_PARAMS}
placeholder={`{\n "enable_ocr": false,\n "enable_formula": true,\n "enable_table": true,\n "language": "en",\n "model_version": "pipeline",\n "page_ranges": ""\n}`}
minSize={100}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('File Extensions')}
description={$i18n.t('Comma-separated extensions MinerU should handle.')}
>
<input
class={inputClass}
placeholder={$i18n.t('pdf, docx, pptx, xlsx')}
bind:value={RAGConfig.MINERU_FILE_EXTENSIONS}
/>
</AdminSettingField>
{/if}
<AdminSettingRow
label={$i18n.t('Bypass Embedding and Retrieval')}
description={RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL
? $i18n.t('Inject the entire content as context for comprehensive processing.')
: $i18n.t('Use segmented retrieval for focused and relevant context.')}
let:labelId
>
<Switch
bind:state={RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
{#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
<AdminSettingRow
label={$i18n.t('Text Splitter')}
description={$i18n.t('Choose how extracted text is split before indexing.')}
>
<SettingsSelect bind:value={RAGConfig.TEXT_SPLITTER}>
<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
<option value="token">{$i18n.t('Token')} ({$i18n.t('Tiktoken')})</option>
<option value="token_transformers">
{$i18n.t('Token')} ({$i18n.t('Transformers')})
</option>
</SettingsSelect>
</AdminSettingRow>
{#if RAGConfig.TEXT_SPLITTER === 'token_transformers'}
<AdminSettingField
label={$i18n.t('Tokenizer Model')}
description={$i18n.t('Tokenizer model used for transformer token splitting.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Enter Tokenizer Model')}
bind:value={RAGConfig.RAG_TOKENIZER_MODEL}
autocomplete="off"
required={RAG_EMBEDDING_ENGINE !== ''}
/>
</AdminSettingField>
{/if}
<AdminSettingRow
label={$i18n.t('Markdown Header Text Splitter')}
description={$i18n.t(
'Split documents by markdown headers before character or token splitting.'
)}
let:labelId
>
<Switch
bind:state={RAGConfig.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('Chunk Size')}
description={$i18n.t('Maximum size for each text chunk.')}
>
<input
class={inputClass}
type="number"
placeholder={$i18n.t('Enter Chunk Size')}
bind:value={RAGConfig.CHUNK_SIZE}
autocomplete="off"
min="0"
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('Chunk Overlap')}
description={$i18n.t('Overlap preserved between neighboring chunks.')}
>
<input
class={inputClass}
type="number"
placeholder={$i18n.t('Enter Chunk Overlap')}
bind:value={RAGConfig.CHUNK_OVERLAP}
autocomplete="off"
min="0"
/>
</AdminSettingField>
</div>
{#if RAGConfig.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER}
<AdminSettingField
label={$i18n.t('Chunk Min Size Target')}
description={$i18n.t(
'Merge chunks smaller than this threshold when possible. Set to 0 to disable merging.'
)}
>
<input
class={inputClass}
type="number"
placeholder={$i18n.t('Enter Chunk Min Size Target')}
bind:value={RAGConfig.CHUNK_MIN_SIZE_TARGET}
autocomplete="off"
min="0"
/>
</AdminSettingField>
{/if}
{/if}
</AdminSettingSection>
{#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
<AdminSettingSection title={$i18n.t('Embedding')}>
<AdminSettingRow
label={$i18n.t('Embedding Model Engine')}
description={$i18n.t('Provider used to generate document embeddings.')}
>
<SettingsSelect
bind:value={RAG_EMBEDDING_ENGINE}
placeholder={$i18n.t('Select an embedding model engine')}
on:change={(e) => {
if (e.target.value === 'ollama') {
RAG_EMBEDDING_MODEL = '';
} else if (e.target.value === 'openai') {
RAG_EMBEDDING_MODEL = 'text-embedding-3-small';
} else if (e.target.value === 'azure_openai') {
RAG_EMBEDDING_MODEL = 'text-embedding-3-small';
} else if (e.target.value === '') {
RAG_EMBEDDING_MODEL = 'sentence-transformers/all-MiniLM-L6-v2';
}
}}
>
<option value="">{$i18n.t('Default (SentenceTransformers)')}</option>
<option value="ollama">{$i18n.t('Ollama')}</option>
<option value="openai">{$i18n.t('OpenAI')}</option>
<option value="azure_openai">{$i18n.t('Azure OpenAI')}</option>
</SettingsSelect>
</AdminSettingRow>
{#if RAG_EMBEDDING_ENGINE === 'openai'}
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('API Base URL')}
description={$i18n.t('OpenAI-compatible embeddings endpoint.')}
>
<input
class={inputClass}
placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl}
required
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Key')}
description={$i18n.t('API key for embedding requests.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('API Key')}
bind:value={OpenAIKey}
required={false}
/>
</AdminSettingField>
</div>
{:else if RAG_EMBEDDING_ENGINE === 'ollama'}
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('API Base URL')}
description={$i18n.t('Ollama endpoint used for embeddings.')}
>
<input
class={inputClass}
placeholder={$i18n.t('API Base URL')}
bind:value={OllamaUrl}
required
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Key')}
description={$i18n.t('Optional API key for Ollama requests.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('API Key')}
bind:value={OllamaKey}
required={false}
/>
</AdminSettingField>
</div>
{:else if RAG_EMBEDDING_ENGINE === 'azure_openai'}
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('API Base URL')}
description={$i18n.t('Azure OpenAI endpoint used for embeddings.')}
>
<input
class={inputClass}
placeholder={$i18n.t('API Base URL')}
bind:value={AzureOpenAIUrl}
required
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Key')}
description={$i18n.t('Azure OpenAI API key.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('API Key')}
bind:value={AzureOpenAIKey}
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('Version')}
description={$i18n.t('Azure OpenAI API version.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Version')}
bind:value={AzureOpenAIVersion}
required
/>
</AdminSettingField>
</div>
{/if}
<AdminSettingField
label={$i18n.t('Embedding Model')}
description={$i18n.t(
'Model used to generate embeddings. Reindex knowledge after changing this.'
)}
>
<div class="flex w-full gap-2">
<input
class={inputClass}
placeholder={RAG_EMBEDDING_ENGINE === 'ollama'
? $i18n.t('Set embedding model')
: $i18n.t('Set embedding model (e.g. {{model}})', {
model: RAG_EMBEDDING_MODEL.slice(-40)
})}
bind:value={RAG_EMBEDDING_MODEL}
required={RAG_EMBEDDING_ENGINE === 'ollama'}
/>
{#if RAG_EMBEDDING_ENGINE === ''}
<button
class="flex size-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-500 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
on:click={() => {
embeddingModelUpdateHandler();
}}
disabled={updateEmbeddingModelLoading}
aria-label={$i18n.t('Update embedding model')}
>
{#if updateEmbeddingModelLoading}
<Spinner />
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
>
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
{/if}
</div>
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t(
'After changing the embedding model, reindex knowledge, knowledge search, and memory vectors for changes to take effect.'
)}
</div>
</AdminSettingField>
<AdminSettingRow
label={$i18n.t('Embedding Batch Size')}
description={$i18n.t('Number of items processed per embedding batch.')}
>
<input
bind:value={RAG_EMBEDDING_BATCH_SIZE}
type="number"
class="w-14 bg-transparent text-center text-xs outline-none"
min="-2"
max="16000"
step="1"
/>
</AdminSettingRow>
{#if RAG_EMBEDDING_ENGINE === 'ollama' || RAG_EMBEDDING_ENGINE === 'openai' || RAG_EMBEDDING_ENGINE === 'azure_openai'}
<AdminSettingRow
label={$i18n.t('Async Embedding Processing')}
description={$i18n.t('Run embedding tasks concurrently to speed up processing.')}
let:labelId
>
<Switch bind:state={ENABLE_ASYNC_EMBEDDING} ariaLabelledbyId={labelId} />
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Embedding Concurrent Requests')}
description={$i18n.t(
'Maximum concurrent embedding requests. Set to 0 for unlimited.'
)}
>
<input
bind:value={RAG_EMBEDDING_CONCURRENT_REQUESTS}
type="number"
class="w-14 bg-transparent text-center text-xs outline-none"
min="0"
step="1"
/>
</AdminSettingRow>
{/if}
</AdminSettingSection>
{/if}
<AdminSettingSection title={$i18n.t('Retrieval')}>
{#if !RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL}
<AdminSettingRow
label={$i18n.t('Full Context Mode')}
description={RAGConfig.RAG_FULL_CONTEXT
? $i18n.t('Inject entire documents as context for comprehensive processing.')
: $i18n.t('Use segmented retrieval for focused context.')}
let:labelId
>
<Switch bind:state={RAGConfig.RAG_FULL_CONTEXT} ariaLabelledbyId={labelId} />
</AdminSettingRow>
{#if !RAGConfig.RAG_FULL_CONTEXT}
<AdminSettingRow
label={$i18n.t('Hybrid Search')}
description={$i18n.t('Combine semantic and keyword retrieval.')}
let:labelId
>
<Switch bind:state={RAGConfig.ENABLE_RAG_HYBRID_SEARCH} ariaLabelledbyId={labelId} />
</AdminSettingRow>
{#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true}
<AdminSettingRow
label={$i18n.t('Enrich Hybrid Search Text')}
description={$i18n.t(
'Add filenames, titles, sections, and snippets to improve lexical recall.'
)}
let:labelId
>
<Switch
bind:state={RAGConfig.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Reranking Engine')}
description={$i18n.t('Provider used to rerank hybrid search results.')}
>
<SettingsSelect
bind:value={RAGConfig.RAG_RERANKING_ENGINE}
placeholder={$i18n.t('Select a reranking model engine')}
on:change={(e) => {
if (e.target.value === 'external') {
RAGConfig.RAG_RERANKING_MODEL = '';
} else if (e.target.value === '') {
RAGConfig.RAG_RERANKING_MODEL = 'BAAI/bge-reranker-v2-m3';
}
}}
>
<option value="">{$i18n.t('Default (SentenceTransformers)')}</option>
<option value="external">{$i18n.t('External')}</option>
</SettingsSelect>
</AdminSettingRow>
{#if RAGConfig.RAG_RERANKING_ENGINE === 'external'}
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('API Base URL')}
description={$i18n.t('External reranker endpoint.')}
>
<input
class={inputClass}
placeholder={$i18n.t('API Base URL')}
bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_URL}
required
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('API Key')}
description={$i18n.t('API key sent to the external reranker.')}
>
<SensitiveInput
variant="settings"
placeholder={$i18n.t('API Key')}
bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_API_KEY}
required={false}
/>
</AdminSettingField>
</div>
{/if}
<AdminSettingField
label={$i18n.t('Reranking Model')}
description={$i18n.t('Model used to rerank retrieved results.')}
>
<input
class={inputClass}
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
model: 'BAAI/bge-reranker-v2-m3'
})}
bind:value={RAGConfig.RAG_RERANKING_MODEL}
/>
</AdminSettingField>
{/if}
<AdminSettingRow
label={$i18n.t('Reranking Batch Size')}
description={$i18n.t('Number of results processed per reranking batch.')}
>
<input
bind:value={RAGConfig.RAG_RERANKING_BATCH_SIZE}
type="number"
class="w-14 bg-transparent text-center text-xs outline-none"
min="1"
max="16000"
step="1"
/>
</AdminSettingRow>
<AdminSettingField
label={$i18n.t('Top K')}
description={$i18n.t('Maximum number of retrieved chunks returned to the model.')}
>
<input
class={inputClass}
type="number"
placeholder={$i18n.t('Enter Top K')}
bind:value={RAGConfig.TOP_K}
autocomplete="off"
min="0"
/>
</AdminSettingField>
{#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true}
<AdminSettingField
label={$i18n.t('Top K Reranker')}
description={$i18n.t('Maximum number of hybrid results sent to the reranker.')}
>
<input
class={inputClass}
type="number"
placeholder={$i18n.t('Enter Top K Reranker')}
bind:value={RAGConfig.TOP_K_RERANKER}
autocomplete="off"
min="0"
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('Relevance Threshold')}
description={$i18n.t(
'Only return documents with a score greater than or equal to this value.'
)}
>
<input
class={inputClass}
type="number"
step="0.01"
placeholder={$i18n.t('Enter Score')}
bind:value={RAGConfig.RELEVANCE_THRESHOLD}
autocomplete="off"
min="0.0"
title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')}
/>
</AdminSettingField>
<AdminSettingRow
label={$i18n.t('BM25 Weight')}
description={$i18n.t('Balance semantic and lexical weighting for hybrid search.')}
>
<button
class={actionButtonClass}
type="button"
on:click={() => {
RAGConfig.HYBRID_BM25_WEIGHT =
(RAGConfig?.HYBRID_BM25_WEIGHT ?? null) === null ? 0.5 : null;
}}
>
{(RAGConfig?.HYBRID_BM25_WEIGHT ?? null) === null
? $i18n.t('Default')
: $i18n.t('Custom')}
</button>
</AdminSettingRow>
{#if (RAGConfig?.HYBRID_BM25_WEIGHT ?? null) !== null}
<div class="flex items-center gap-2">
<div class="flex-1">
<input
id="steps-range"
type="range"
min="0"
max="1"
step="0.05"
bind:value={RAGConfig.HYBRID_BM25_WEIGHT}
class="h-2 w-full cursor-pointer appearance-none rounded-lg dark:bg-gray-700"
/>
<div
class="flex justify-between py-0.5 text-[0.6875rem] text-gray-400 dark:text-gray-600"
>
<div>{$i18n.t('semantic')}</div>
<div>{$i18n.t('lexical')}</div>
</div>
</div>
<input
bind:value={RAGConfig.HYBRID_BM25_WEIGHT}
type="number"
class="w-14 bg-transparent text-center text-xs outline-none"
min="0"
max="1"
step="any"
/>
</div>
{/if}
{/if}
{/if}
{/if}
<AdminSettingField
label={$i18n.t('RAG Template')}
description={$i18n.t('Prompt template used when retrieved context is injected.')}
>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
className="w-full"
>
<Textarea
className={textareaClass}
bind:value={RAGConfig.RAG_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
{#if RAGConfig.RAG_TEMPLATE && (RAGConfig.RAG_TEMPLATE.match(/\[context\]/g) || []).length + (RAGConfig.RAG_TEMPLATE.match(/\{\{CONTEXT\}\}/g) || []).length > 1}
<div class="mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
{$i18n.t(
'This template contains multiple context placeholders ([context] or {{CONTEXT}}). Context will be injected at each occurrence.'
)}
</div>
{/if}
</AdminSettingField>
</AdminSettingSection>
<AdminSettingSection title={$i18n.t('Files')}>
<AdminSettingField
label={$i18n.t('Allowed File Extensions')}
description={$i18n.t(
'Comma-separated upload extensions. Leave empty for all file types.'
)}
>
<input
class={inputClass}
type="text"
placeholder={$i18n.t('e.g. pdf, docx, txt')}
bind:value={RAGConfig.ALLOWED_FILE_EXTENSIONS}
autocomplete="off"
/>
</AdminSettingField>
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('Max Upload Size')}
description={$i18n.t('Maximum file size in MB. Leave empty for unlimited.')}
>
<input
class={inputClass}
type="number"
placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_SIZE}
autocomplete="off"
min="0"
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('Max Upload Count')}
description={$i18n.t('Maximum number of files that can be used at once in chat.')}
>
<input
class={inputClass}
type="number"
placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_COUNT}
autocomplete="off"
min="0"
/>
</AdminSettingField>
</div>
<div class="grid grid-cols-1 gap-x-3 gap-y-2.5 sm:grid-cols-2">
<AdminSettingField
label={$i18n.t('Image Compression Width')}
description={$i18n.t(
'Width in pixels to compress images to. Leave empty for no compression.'
)}
>
<input
class={inputClass}
type="number"
placeholder={$i18n.t('Leave empty for no compression')}
bind:value={RAGConfig.FILE_IMAGE_COMPRESSION_WIDTH}
autocomplete="off"
min="0"
/>
</AdminSettingField>
<AdminSettingField
label={$i18n.t('Image Compression Height')}
description={$i18n.t(
'Height in pixels to compress images to. Leave empty for no compression.'
)}
>
<input
class={inputClass}
type="number"
placeholder={$i18n.t('Leave empty for no compression')}
bind:value={RAGConfig.FILE_IMAGE_COMPRESSION_HEIGHT}
autocomplete="off"
min="0"
/>
</AdminSettingField>
</div>
</AdminSettingSection>
<AdminSettingSection title={$i18n.t('Integration')}>
<AdminSettingRow
label={$i18n.t('Google Drive')}
description={$i18n.t('Allow Google Drive as a document source.')}
let:labelId
>
<Switch
bind:state={RAGConfig.ENABLE_GOOGLE_DRIVE_INTEGRATION}
ariaLabelledbyId={labelId}
/>
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('OneDrive')}
description={$i18n.t('Allow OneDrive as a document source.')}
let:labelId
>
<Switch bind:state={RAGConfig.ENABLE_ONEDRIVE_INTEGRATION} ariaLabelledbyId={labelId} />
</AdminSettingRow>
</AdminSettingSection>
<AdminSettingSection title={$i18n.t('Danger Zone')}>
<AdminSettingRow
label={$i18n.t('Reset Upload Directory')}
description={$i18n.t('Delete uploaded files from the upload directory.')}
>
<button
class={actionButtonClass}
type="button"
on:click={() => {
showResetUploadDirConfirm = true;
}}
>
{$i18n.t('Reset')}
</button>
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Reset Vector Storage/Knowledge')}
description={$i18n.t('Clear vector storage and knowledge indexing data.')}
>
<button
class={actionButtonClass}
type="button"
on:click={() => {
showResetConfirm = true;
}}
>
{$i18n.t('Reset')}
</button>
</AdminSettingRow>
<AdminSettingRow
label={$i18n.t('Reindex Knowledge and Memory Vectors')}
description={$i18n.t(
'Rebuild vectors for existing knowledge files, knowledge search, and memories.'
)}
>
<button
class={actionButtonClass}
type="button"
on:click={() => {
showReindexConfirm = true;
}}
>
{$i18n.t('Reindex')}
</button>
</AdminSettingRow>
</AdminSettingSection>
</div>
<div class="flex justify-end pt-6 text-sm font-normal">
<button
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
{:else}
<div class="flex items-center justify-center h-full">
<Spinner className="size-5" />
</div>
{/if}
</form>