| | <script lang="ts"> |
| | import { getContext, onMount } from 'svelte'; |
| | import { config, knowledge, settings, user } from '$lib/stores'; |
| | |
| | import KnowledgeSelector from './Knowledge/KnowledgeSelector.svelte'; |
| | import FileItem from '$lib/components/common/FileItem.svelte'; |
| | |
| | import { getKnowledgeBases } from '$lib/apis/knowledge'; |
| | import { uploadFile } from '$lib/apis/files'; |
| | |
| | import { toast } from 'svelte-sonner'; |
| | import { v4 as uuidv4 } from 'uuid'; |
| | import { WEBUI_API_BASE_URL } from '$lib/constants'; |
| | |
| | export let selectedItems = []; |
| | const i18n = getContext('i18n'); |
| | |
| | let loaded = false; |
| | |
| | let filesInputElement = null; |
| | let inputFiles = null; |
| | |
| | $: if (selectedItems === null) { |
| | selectedItems = []; |
| | } |
| | |
| | const uploadFileHandler = async (file, fullContext: boolean = false) => { |
| | if ($user?.role !== 'admin' && !($user?.permissions?.chat?.file_upload ?? true)) { |
| | toast.error($i18n.t('You do not have permission to upload files.')); |
| | return null; |
| | } |
| | |
| | const tempItemId = uuidv4(); |
| | const fileItem = { |
| | type: 'file', |
| | file: '', |
| | id: null, |
| | url: '', |
| | name: file.name, |
| | collection_name: '', |
| | status: 'uploading', |
| | size: file.size, |
| | error: '', |
| | itemId: tempItemId, |
| | ...(fullContext ? { context: 'full' } : {}) |
| | }; |
| | |
| | if (fileItem.size == 0) { |
| | toast.error($i18n.t('You cannot upload an empty file.')); |
| | return null; |
| | } |
| | |
| | selectedItems = [...selectedItems, fileItem]; |
| | |
| | try { |
| | // If the file is an audio file, provide the language for STT. |
| | let metadata = null; |
| | if ( |
| | (file.type.startsWith('audio/') || file.type.startsWith('video/')) && |
| | $settings?.audio?.stt?.language |
| | ) { |
| | metadata = { |
| | language: $settings?.audio?.stt?.language |
| | }; |
| | } |
| | |
| | |
| | const uploadedFile = await uploadFile(localStorage.token, file, metadata); |
| | |
| | if (uploadedFile) { |
| | console.log('File upload completed:', { |
| | id: uploadedFile.id, |
| | name: fileItem.name, |
| | collection: uploadedFile?.meta?.collection_name |
| | }); |
| | |
| | if (uploadedFile.error) { |
| | console.warn('File upload warning:', uploadedFile.error); |
| | toast.warning(uploadedFile.error); |
| | } |
| | |
| | fileItem.status = 'uploaded'; |
| | fileItem.file = uploadedFile; |
| | fileItem.id = uploadedFile.id; |
| | fileItem.collection_name = |
| | uploadedFile?.meta?.collection_name || uploadedFile?.collection_name; |
| | fileItem.url = `${uploadedFile.id}`; |
| | |
| | selectedItems = selectedItems; |
| | } else { |
| | selectedItems = selectedItems.filter((item) => item?.itemId !== tempItemId); |
| | } |
| | } catch (e) { |
| | toast.error(`${e}`); |
| | selectedItems = selectedItems.filter((item) => item?.itemId !== tempItemId); |
| | } |
| | }; |
| | |
| | const inputFilesHandler = async (inputFiles) => { |
| | console.log('Input files handler called with:', inputFiles); |
| | |
| | inputFiles.forEach(async (file) => { |
| | console.log('Processing file:', { |
| | name: file.name, |
| | type: file.type, |
| | size: file.size, |
| | extension: file.name.split('.').at(-1) |
| | }); |
| | |
| | if ( |
| | ($config?.file?.max_size ?? null) !== null && |
| | file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024 |
| | ) { |
| | console.log('File exceeds max size limit:', { |
| | fileSize: file.size, |
| | maxSize: ($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 (!file['type'].startsWith('image/')) { |
| | uploadFileHandler(file); |
| | } else { |
| | toast.error($i18n.t(`Unsupported file type.`)); |
| | } |
| | }); |
| | }; |
| | |
| | onMount(async () => { |
| | loaded = true; |
| | }); |
| | </script> |
| |
|
| | <input |
| | bind:this={filesInputElement} |
| | bind:files={inputFiles} |
| | type="file" |
| | hidden |
| | multiple |
| | on:change={async () => { |
| | if (inputFiles && inputFiles.length > 0) { |
| | const _inputFiles = Array.from(inputFiles); |
| | inputFilesHandler(_inputFiles); |
| | } else { |
| | toast.error($i18n.t(`File not found.`)); |
| | } |
| |
|
| | filesInputElement.value = ''; |
| | }} |
| | /> |
| |
|
| | <div> |
| | <slot name="label"> |
| | <div class="mb-2"> |
| | <div class="flex w-full justify-between mb-1"> |
| | <div class=" self-center text-xs font-medium text-gray-500"> |
| | {$i18n.t('Knowledge')} |
| | </div> |
| | </div> |
| | </div> |
| | </slot> |
| | |
| | <div class="flex flex-col mb-1"> |
| | {#if selectedItems?.length > 0} |
| | <div class=" flex flex-wrap items-center gap-2 mb-2.5"> |
| | {#each selectedItems as file, fileIdx} |
| | <FileItem |
| | {file} |
| | small={true} |
| | item={file} |
| | name={file.name} |
| | modal={true} |
| | edit={true} |
| | loading={file.status === 'uploading'} |
| | type={file?.legacy |
| | ? `Legacy${file.type ? ` ${file.type}` : ''}` |
| | : (file?.type ?? 'collection')} |
| | dismissible |
| | on:dismiss={(e) => { |
| | selectedItems = selectedItems.filter((_, idx) => idx !== fileIdx); |
| | }} |
| | /> |
| | {/each} |
| | </div> |
| | {/if} |
| | |
| | {#if loaded} |
| | <div class="flex flex-wrap flex-row text-sm gap-1"> |
| | <KnowledgeSelector |
| | on:select={(e) => { |
| | const item = e.detail; |
| | |
| | if (!selectedItems.find((k) => k.id === item.id)) { |
| | selectedItems = [ |
| | ...selectedItems, |
| | { |
| | ...item |
| | } |
| | ]; |
| | } |
| | }} |
| | > |
| | <div |
| | class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-850 rounded-3xl" |
| | > |
| | {$i18n.t('Select Knowledge')} |
| | </div> |
| | </KnowledgeSelector> |
| | |
| | {#if $user?.role === 'admin' || $user?.permissions?.chat?.file_upload} |
| | <button |
| | class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-850 rounded-3xl" |
| | type="button" |
| | on:click={() => { |
| | filesInputElement.click(); |
| | }}>{$i18n.t('Upload Files')}</button |
| | > |
| | {/if} |
| | </div> |
| | {/if} |
| | <!-- {knowledge} --> |
| | </div> |
| |
|
| | <div class=" text-xs dark:text-gray-700"> |
| | {$i18n.t('To attach knowledge base here, add them to the "Knowledge" workspace first.')} |
| | </div> |
| | </div> |
| |
|