| <script lang="ts"> |
| import Fuse from 'fuse.js'; |
| import { toast } from 'svelte-sonner'; |
| import { v4 as uuidv4 } from 'uuid'; |
| import { PaneGroup, Pane, PaneResizer } from 'paneforge'; |
| |
| import { onMount, getContext, onDestroy, tick } from 'svelte'; |
| const i18n = getContext('i18n'); |
| |
| import { goto } from '$app/navigation'; |
| import { page } from '$app/stores'; |
| import { |
| mobile, |
| showSidebar, |
| knowledge as _knowledge, |
| config, |
| user, |
| settings |
| } from '$lib/stores'; |
| |
| import { |
| updateFileDataContentById, |
| uploadFile, |
| deleteFileById, |
| getFileById, |
| renameFileById |
| } from '$lib/apis/files'; |
| import { |
| addFileToKnowledgeById, |
| getKnowledgeById, |
| getPendingKnowledgeFiles, |
| removeFileFromKnowledgeById, |
| resetKnowledgeById, |
| updateFileFromKnowledgeById, |
| updateKnowledgeById, |
| updateKnowledgeAccessGrants, |
| searchKnowledgeFilesById, |
| createKnowledgeDirectory, |
| updateKnowledgeDirectory, |
| deleteKnowledgeDirectory, |
| moveFileInKnowledge, |
| syncKnowledgeDiff, |
| syncKnowledgeCleanup |
| } from '$lib/apis/knowledge'; |
| import { processWeb, processYoutubeVideo } from '$lib/apis/retrieval'; |
| |
| import { blobToFile, isYoutubeUrl, copyToClipboard } from '$lib/utils'; |
| import { computeFileHash } from '$lib/utils/hash'; |
| |
| import Spinner from '$lib/components/common/Spinner.svelte'; |
| import Tooltip from '$lib/components/common/Tooltip.svelte'; |
| import Files from './KnowledgeBase/Files.svelte'; |
| import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte'; |
| |
| import AddContentMenu from './KnowledgeBase/AddContentMenu.svelte'; |
| import AddTextContentModal from './KnowledgeBase/AddTextContentModal.svelte'; |
| import NewDirectoryModal from './KnowledgeBase/NewDirectoryModal.svelte'; |
| import KnowledgeBreadcrumbs from './KnowledgeBase/KnowledgeBreadcrumbs.svelte'; |
| |
| import SyncConfirmDialog from '../../common/ConfirmDialog.svelte'; |
| import ConfirmDialog from '../../common/ConfirmDialog.svelte'; |
| import Drawer from '$lib/components/common/Drawer.svelte'; |
| import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte'; |
| import LockClosed from '$lib/components/icons/LockClosed.svelte'; |
| import AccessControlModal from '../common/AccessControlModal.svelte'; |
| import Search from '$lib/components/icons/Search.svelte'; |
| import FilesOverlay from '$lib/components/chat/MessageInput/FilesOverlay.svelte'; |
| import DropdownOptions from '$lib/components/common/DropdownOptions.svelte'; |
| import Dropdown from '$lib/components/common/Dropdown.svelte'; |
| import Checkbox from '$lib/components/common/Checkbox.svelte'; |
| import AdjustmentsHorizontal from '$lib/components/icons/AdjustmentsHorizontal.svelte'; |
| import Pagination from '$lib/components/common/Pagination.svelte'; |
| import AttachWebpageModal from '$lib/components/chat/MessageInput/AttachWebpageModal.svelte'; |
| |
| let largeScreen = true; |
| |
| let pane; |
| let showSidepanel = true; |
| |
| let showAddWebpageModal = false; |
| let showAddTextContentModal = false; |
| let showNewDirectoryModal = false; |
| |
| let showSyncConfirmModal = false; |
| let pendingSyncFiles: Array<{ path: string; filename: string; file: File }> | null = null; |
| let syncing: string | null = null; |
| let showAccessControlModal = false; |
| let showResetConfirm = false; |
| |
| let minSize = 0; |
| type Knowledge = { |
| id: string; |
| name: string; |
| description: string; |
| data: { |
| file_ids: string[]; |
| }; |
| files: any[]; |
| access_grants?: any[]; |
| write_access?: boolean; |
| }; |
| |
| let id = null; |
| let knowledge: Knowledge | null = null; |
| let knowledgeId = null; |
| |
| let selectedFileId = null; |
| let selectedFile = null; |
| let selectedFileContent = ''; |
| |
| let inputFiles = null; |
| |
| let query = ''; |
| let includeContent = false; |
| let searchDebounceTimer: ReturnType<typeof setTimeout>; |
| |
| let viewOption = null; |
| let sortKey = null; |
| let direction = null; |
| |
| let currentPage = 1; |
| let fileItems = null; |
| let fileItemsTotal = null; |
| |
| |
| let currentDirectoryId: string | null = null; |
| let directoryItems = []; |
| let breadcrumbs = []; |
| |
| let showDeleteDirectoryConfirm = false; |
| let pendingDeleteDirectoryId: string | null = null; |
| let deleteDirectoryContents = true; |
| |
| let pendingPollTimer: ReturnType<typeof setInterval> | null = null; |
| |
| const reset = () => { |
| currentPage = 1; |
| }; |
| |
| const init = async () => { |
| reset(); |
| await getItemsPage(); |
| }; |
| |
| |
| $: if (query !== undefined) { |
| clearTimeout(searchDebounceTimer); |
| |
| searchDebounceTimer = setTimeout(() => { |
| getItemsPage(); |
| }, 300); |
| } |
| |
| |
| $: if ( |
| knowledgeId !== null && |
| viewOption !== undefined && |
| sortKey !== undefined && |
| direction !== undefined && |
| currentPage !== undefined && |
| includeContent !== undefined |
| ) { |
| getItemsPage(); |
| } |
| |
| $: if ( |
| query !== undefined && |
| viewOption !== undefined && |
| sortKey !== undefined && |
| direction !== undefined |
| ) { |
| reset(); |
| } |
| |
| const getItemsPage = async () => { |
| if (knowledgeId === null) return; |
| |
| fileItems = null; |
| fileItemsTotal = null; |
| |
| if (sortKey === null) { |
| direction = null; |
| } |
| |
| const res = await searchKnowledgeFilesById( |
| localStorage.token, |
| knowledge.id, |
| query, |
| viewOption, |
| sortKey, |
| direction, |
| currentPage, |
| currentDirectoryId, |
| includeContent |
| ).catch(() => { |
| return null; |
| }); |
| |
| if (res) { |
| fileItems = res.items; |
| fileItemsTotal = res.total; |
| directoryItems = res.directories ?? []; |
| breadcrumbs = res.breadcrumbs ?? []; |
| |
| // Merge in-flight files not yet linked to the knowledge base |
| try { |
| const pendingFiles = await getPendingKnowledgeFiles(localStorage.token, knowledgeId); |
| if (pendingFiles && pendingFiles.length > 0) { |
| const existingIds = new Set(fileItems.map((f) => f.id)); |
| const newPending = pendingFiles |
| .filter((f) => !existingIds.has(f.id)) |
| .map((f) => ({ |
| ...f, |
| name: f.meta?.name ?? f.filename, |
| status: 'uploading' |
| })); |
| if (newPending.length > 0) { |
| fileItems = [...newPending, ...fileItems]; |
| |
| // Start polling for completion (if not already polling) |
| if (!pendingPollTimer) { |
| pendingPollTimer = setInterval(async () => { |
| try { |
| const still = await getPendingKnowledgeFiles(localStorage.token, knowledgeId); |
| if (!still || still.length === 0) { |
| clearInterval(pendingPollTimer); |
| pendingPollTimer = null; |
| init(); |
| } |
| } catch {} |
| }, 5000); |
| } |
| } |
| } |
| } catch (e) { |
| console.warn('Failed to fetch pending files:', e); |
| } |
| } |
| |
| return res; |
| }; |
| |
| const fileSelectHandler = async (file) => { |
| try { |
| selectedFile = file; |
| selectedFileContent = selectedFile?.data?.content || ''; |
| } catch (e) { |
| toast.error($i18n.t('Failed to load file content.')); |
| } |
| }; |
| |
| const createFileFromText = (name, content) => { |
| const blob = new Blob([content], { type: 'text/plain' }); |
| const file = blobToFile(blob, `${name}.txt`); |
| |
| console.log(file); |
| return file; |
| }; |
| |
| const uploadWeb = async (urls) => { |
| if (!Array.isArray(urls)) { |
| urls = [urls]; |
| } |
| |
| const newFileItems = urls.map((url) => ({ |
| type: 'file', |
| file: '', |
| id: null, |
| url: url, |
| name: url, |
| size: null, |
| status: 'uploading', |
| error: '', |
| itemId: uuidv4() |
| })); |
| |
| |
| fileItems = [...newFileItems, ...(fileItems ?? [])]; |
| |
| for (const fileItem of newFileItems) { |
| try { |
| console.log(fileItem); |
| const res = await processWeb(localStorage.token, '', fileItem.url, false).catch((e) => { |
| console.error('Error processing web URL:', e); |
| return null; |
| }); |
| |
| if (res) { |
| console.log(res); |
| const file = createFileFromText( |
| // Use URL as filename, sanitized |
| fileItem.url |
| .replace(/[^a-z0-9]/gi, '_') |
| .toLowerCase() |
| .slice(0, 50), |
| res.content |
| ); |
| |
| const uploadedFile = await uploadFile(localStorage.token, file, { |
| knowledge_id: knowledge.id, |
| directory_id: currentDirectoryId |
| }).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (uploadedFile) { |
| console.log(uploadedFile); |
| fileItems = fileItems.map((item) => { |
| if (item.itemId === fileItem.itemId) { |
| item.id = uploadedFile.id; |
| } |
| return item; |
| }); |
| |
| if (uploadedFile.error) { |
| console.warn('File upload warning:', uploadedFile.error); |
| toast.warning(uploadedFile.error); |
| fileItems = fileItems.filter((file) => file.id !== uploadedFile.id); |
| } else { |
| toast.success($i18n.t('File added successfully.')); |
| init(); |
| } |
| } else { |
| toast.error($i18n.t('Failed to upload file.')); |
| } |
| } else { |
| // remove the item from fileItems |
| fileItems = fileItems.filter((item) => item.itemId !== fileItem.itemId); |
| toast.error($i18n.t('Failed to process URL: {{url}}', { url: fileItem.url })); |
| } |
| } catch (e) { |
| // remove the item from fileItems |
| fileItems = fileItems.filter((item) => item.itemId !== fileItem.itemId); |
| toast.error(`${e}`); |
| } |
| } |
| }; |
| |
| const uploadFileHandler = async (file) => { |
| console.log(file); |
| |
| const fileItem = { |
| type: 'file', |
| file: '', |
| id: null, |
| url: '', |
| name: file.name, |
| size: file.size, |
| status: 'uploading', |
| error: '', |
| itemId: uuidv4() |
| }; |
| |
| if (fileItem.size == 0) { |
| toast.error($i18n.t('You cannot upload an empty file.')); |
| return null; |
| } |
| |
| 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; |
| } |
| |
| fileItems = [fileItem, ...(fileItems ?? [])]; |
| try { |
| let metadata = { |
| knowledge_id: knowledge.id, |
| directory_id: currentDirectoryId, |
| // If the file is an audio file, provide the language for STT. |
| ...((file.type.startsWith('audio/') || file.type.startsWith('video/')) && |
| $settings?.audio?.stt?.language |
| ? { |
| language: $settings?.audio?.stt?.language |
| } |
| : {}) |
| }; |
| |
| const uploadedFile = await uploadFile(localStorage.token, file, metadata).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (uploadedFile) { |
| console.log(uploadedFile); |
| fileItems = fileItems.map((item) => { |
| if (item.itemId === fileItem.itemId) { |
| item.id = uploadedFile.id; |
| } |
| return item; |
| }); |
| |
| if (uploadedFile.error) { |
| console.warn('File upload warning:', uploadedFile.error); |
| toast.warning(uploadedFile.error); |
| fileItems = fileItems.filter((file) => file.id !== uploadedFile.id); |
| } else { |
| toast.success($i18n.t('File added successfully.')); |
| init(); |
| } |
| } else { |
| toast.error($i18n.t('Failed to upload file.')); |
| } |
| } catch (e) { |
| toast.error(`${e}`); |
| } |
| }; |
| |
| const uploadDirectoryHandler = async () => { |
| // Check if File System Access API is supported |
| const isFileSystemAccessSupported = 'showDirectoryPicker' in window; |
| |
| try { |
| if (isFileSystemAccessSupported) { |
| // Modern browsers (Chrome, Edge) implementation |
| await handleModernBrowserUpload(); |
| } else { |
| // Firefox fallback |
| await handleFirefoxUpload(); |
| } |
| } catch (error) { |
| handleUploadError(error); |
| } |
| }; |
| |
| |
| const hasHiddenFolder = (path) => { |
| return path.split('/').some((part) => part.startsWith('.')); |
| }; |
| |
| |
| const handleModernBrowserUpload = async () => { |
| const dirHandle = await window.showDirectoryPicker(); |
| let totalFiles = 0; |
| let uploadedFiles = 0; |
| |
| // Function to update the UI with the progress |
| const updateProgress = () => { |
| const percentage = (uploadedFiles / totalFiles) * 100; |
| toast.info( |
| $i18n.t('Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)', { |
| uploadedFiles: uploadedFiles, |
| totalFiles: totalFiles, |
| percentage: percentage.toFixed(2) |
| }) |
| ); |
| }; |
| |
| |
| async function countFiles(dirHandle) { |
| for await (const entry of dirHandle.values()) { |
| // Skip hidden files and directories |
| if (entry.name.startsWith('.')) continue; |
| |
| if (entry.kind === 'file') { |
| totalFiles++; |
| } else if (entry.kind === 'directory') { |
| // Only process non-hidden directories |
| if (!entry.name.startsWith('.')) { |
| await countFiles(entry); |
| } |
| } |
| } |
| } |
| |
| |
| async function processDirectory(dirHandle, path = '') { |
| for await (const entry of dirHandle.values()) { |
| // Skip hidden files and directories |
| if (entry.name.startsWith('.')) continue; |
| |
| const entryPath = path ? `${path}/${entry.name}` : entry.name; |
| |
| // Skip if the path contains any hidden folders |
| if (hasHiddenFolder(entryPath)) continue; |
| |
| if (entry.kind === 'file') { |
| const file = await entry.getFile(); |
| const fileWithPath = new File([file], entryPath, { type: file.type }); |
| |
| await uploadFileHandler(fileWithPath); |
| uploadedFiles++; |
| updateProgress(); |
| } else if (entry.kind === 'directory') { |
| // Only process non-hidden directories |
| if (!entry.name.startsWith('.')) { |
| await processDirectory(entry, entryPath); |
| } |
| } |
| } |
| } |
| |
| await countFiles(dirHandle); |
| updateProgress(); |
| |
| if (totalFiles > 0) { |
| await processDirectory(dirHandle); |
| } else { |
| console.log('No files to upload.'); |
| } |
| }; |
| |
| |
| const handleFirefoxUpload = async () => { |
| return new Promise((resolve, reject) => { |
| // Create hidden file input |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.webkitdirectory = true; |
| input.directory = true; |
| input.multiple = true; |
| input.style.display = 'none'; |
| |
| // Add input to DOM temporarily |
| document.body.appendChild(input); |
| |
| input.onchange = async () => { |
| try { |
| const files = Array.from(input.files) |
| // Filter out files from hidden folders |
| .filter((file) => !hasHiddenFolder(file.webkitRelativePath)); |
| |
| let totalFiles = files.length; |
| let uploadedFiles = 0; |
| |
| // Function to update the UI with the progress |
| const updateProgress = () => { |
| const percentage = (uploadedFiles / totalFiles) * 100; |
| toast.info( |
| $i18n.t('Upload Progress: {{uploadedFiles}}/{{totalFiles}} ({{percentage}}%)', { |
| uploadedFiles: uploadedFiles, |
| totalFiles: totalFiles, |
| percentage: percentage.toFixed(2) |
| }) |
| ); |
| }; |
| |
| updateProgress(); |
| |
| |
| for (const file of files) { |
| // Skip hidden files (additional check) |
| if (!file.name.startsWith('.')) { |
| const relativePath = file.webkitRelativePath || file.name; |
| const fileWithPath = new File([file], relativePath, { type: file.type }); |
| |
| await uploadFileHandler(fileWithPath); |
| uploadedFiles++; |
| updateProgress(); |
| } |
| } |
| |
| |
| document.body.removeChild(input); |
| resolve(); |
| } catch (error) { |
| reject(error); |
| } |
| }; |
| |
| input.onerror = (error) => { |
| document.body.removeChild(input); |
| reject(error); |
| }; |
| |
| |
| input.click(); |
| }); |
| }; |
| |
| |
| const handleUploadError = (error) => { |
| if (error.name === 'AbortError') { |
| toast.info($i18n.t('Directory selection was cancelled')); |
| } else { |
| toast.error($i18n.t('Error accessing directory')); |
| console.error('Directory access error:', error); |
| } |
| }; |
| |
| {path, filename, file}[] |
| const collectDirectoryFiles = async (): Promise<Array<{ |
| path: string; |
| filename: string; |
| file: File; |
| }> | null> => { |
| const isFileSystemAccessSupported = 'showDirectoryPicker' in window; |
| |
| try { |
| if (isFileSystemAccessSupported) { |
| const dirHandle = await window.showDirectoryPicker(); |
| const collected: Array<{ path: string; filename: string; file: File }> = []; |
| |
| async function traverse(handle: FileSystemDirectoryHandle, dirPath = '') { |
| for await (const entry of handle.values()) { |
| if (entry.name.startsWith('.')) continue; |
| const entryPath = dirPath ? `${dirPath}/${entry.name}` : entry.name; |
| if (hasHiddenFolder(entryPath)) continue; |
| |
| if (entry.kind === 'file') { |
| const file = await entry.getFile(); |
| collected.push({ path: dirPath, filename: entry.name, file }); |
| } else if (entry.kind === 'directory') { |
| await traverse(entry, entryPath); |
| } |
| } |
| } |
| |
| await traverse(dirHandle); |
| return collected; |
| } else { |
| // Firefox fallback |
| return new Promise((resolve, reject) => { |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.webkitdirectory = true; |
| input.directory = true; |
| input.multiple = true; |
| input.style.display = 'none'; |
| document.body.appendChild(input); |
| |
| input.onchange = () => { |
| try { |
| const files = Array.from(input.files || []).filter( |
| (file) => !hasHiddenFolder(file.webkitRelativePath) && !file.name.startsWith('.') |
| ); |
| |
| const collected = files.map((file) => { |
| const parts = file.webkitRelativePath.split('/'); |
| // Remove root dir name, extract path and filename |
| const withoutRoot = parts.slice(1); |
| const filename = withoutRoot.pop() || file.name; |
| const path = withoutRoot.join('/'); |
| return { path, filename, file }; |
| }); |
| |
| document.body.removeChild(input); |
| resolve(collected); |
| } catch (error) { |
| document.body.removeChild(input); |
| reject(error); |
| } |
| }; |
| |
| input.onerror = (error) => { |
| document.body.removeChild(input); |
| reject(error); |
| }; |
| |
| input.click(); |
| }); |
| } |
| } catch (error) { |
| handleUploadError(error); |
| return null; |
| } |
| }; |
| |
| |
| const syncDirectoryHandler = async () => { |
| if (!pendingSyncFiles?.length) return; |
| |
| try { |
| // ── 2. Compute checksums ── |
| syncing = $i18n.t('Computing checksums ({{count}} files)', { |
| count: pendingSyncFiles.length |
| }); |
| const manifest = await Promise.all( |
| pendingSyncFiles.map(async (entry) => ({ |
| ...entry, |
| checksum: await computeFileHash(entry.file), |
| size: entry.file.size |
| })) |
| ); |
| pendingSyncFiles = null; |
| |
| |
| syncing = $i18n.t('Comparing with knowledge base...'); |
| const diff = await syncKnowledgeDiff( |
| localStorage.token, |
| id, |
| manifest.map(({ filename, path, checksum, size }) => ({ filename, path, checksum, size })) |
| ); |
| |
| if (!diff) { |
| toast.error($i18n.t('Failed to compare files.')); |
| return; |
| } |
| |
| |
| const staleFileIds = [ |
| ...diff.deleted.map((d: any) => d.file_id), |
| ...diff.modified.map((m: any) => m.stale_file_id) |
| ]; |
| |
| if (staleFileIds.length > 0 || diff.rmdir.length > 0) { |
| syncing = $i18n.t('Removing {{count}} stale files...', { count: staleFileIds.length }); |
| await syncKnowledgeCleanup(localStorage.token, id, staleFileIds, diff.rmdir); |
| } |
| |
| |
| const directoryIdByPath: Record<string, string> = { ...(diff.directory_map || {}) }; |
| for (const dirPath of diff.mkdir) { |
| const segments = dirPath.split('/'); |
| const name = segments.at(-1)!; |
| const parentPath = segments.slice(0, -1).join('/'); |
| const parentId = parentPath ? directoryIdByPath[parentPath] : null; |
| |
| const directory = await createKnowledgeDirectory( |
| localStorage.token, |
| knowledge.id, |
| name, |
| parentId |
| ); |
| if (directory) { |
| directoryIdByPath[dirPath] = directory.id; |
| } |
| } |
| |
| |
| const filesToUpload = manifest.filter( |
| (entry) => |
| diff.added.some((a: any) => a.filename === entry.filename && a.path === entry.path) || |
| diff.modified.some((m: any) => m.filename === entry.filename && m.path === entry.path) |
| ); |
| |
| let uploadedCount = 0; |
| for (const entry of filesToUpload) { |
| uploadedCount++; |
| const displayPath = entry.path ? `${entry.path}/${entry.filename}` : entry.filename; |
| syncing = $i18n.t('Uploading {{current}}/{{total}}: {{file}}', { |
| current: uploadedCount, |
| total: filesToUpload.length, |
| file: displayPath |
| }); |
| |
| const fileObject = new File([entry.file], entry.filename, { type: entry.file.type }); |
| await uploadFile(localStorage.token, fileObject, { |
| knowledge_id: knowledge.id, |
| file_hash: entry.checksum, |
| directory_id: entry.path ? directoryIdByPath[entry.path] : null |
| }).catch(() => null); |
| } |
| |
| |
| toast.success( |
| $i18n.t( |
| 'Sync complete: {{added}} added, {{modified}} modified, {{deleted}} deleted, {{unmodified}} unmodified', |
| { |
| added: diff.added.length, |
| modified: diff.modified.length, |
| deleted: diff.deleted.length, |
| unmodified: diff.unmodified_count |
| } |
| ) |
| ); |
| init(); |
| } catch (e) { |
| toast.error(`${e}`); |
| } finally { |
| syncing = null; |
| } |
| }; |
| |
| const addFileHandler = async (fileId) => { |
| const res = await addFileToKnowledgeById( |
| localStorage.token, |
| id, |
| fileId, |
| currentDirectoryId |
| ).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (res) { |
| toast.success($i18n.t('File added successfully.')); |
| init(); |
| } else { |
| toast.error($i18n.t('Failed to add file.')); |
| fileItems = fileItems.filter((file) => file.id !== fileId); |
| } |
| }; |
| |
| |
| const navigateToDirectory = (directoryId: string | null) => { |
| currentDirectoryId = directoryId; |
| currentPage = 1; |
| selectedFileId = null; |
| selectedFile = null; |
| getItemsPage(); |
| }; |
| |
| const createDirectoryHandler = async (name: string) => { |
| const res = await createKnowledgeDirectory( |
| localStorage.token, |
| knowledge.id, |
| name, |
| currentDirectoryId |
| ).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (res) { |
| toast.success($i18n.t('Directory created.')); |
| getItemsPage(); |
| } |
| }; |
| |
| const renameDirectoryHandler = async (dirId: string, name: string) => { |
| const res = await updateKnowledgeDirectory(localStorage.token, knowledge.id, dirId, { |
| name |
| }).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (res) { |
| toast.success($i18n.t('Directory renamed.')); |
| getItemsPage(); |
| } |
| }; |
| |
| const confirmDeleteDirectory = (dirId: string) => { |
| pendingDeleteDirectoryId = dirId; |
| showDeleteDirectoryConfirm = true; |
| }; |
| |
| const deleteDirectoryHandler = async (moveFiles: boolean) => { |
| if (!pendingDeleteDirectoryId) return; |
| |
| const res = await deleteKnowledgeDirectory( |
| localStorage.token, |
| knowledge.id, |
| pendingDeleteDirectoryId, |
| moveFiles |
| ).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (res) { |
| toast.success($i18n.t('Directory deleted.')); |
| getItemsPage(); |
| } |
| pendingDeleteDirectoryId = null; |
| }; |
| |
| const moveFileToDirectoryHandler = async (fileId: string, directoryId: string | null) => { |
| const res = await moveFileInKnowledge( |
| localStorage.token, |
| knowledge.id, |
| fileId, |
| directoryId |
| ).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (res) { |
| toast.success($i18n.t('File moved.')); |
| getItemsPage(); |
| } |
| }; |
| |
| const moveDirectoryHandler = async (dirId: string, targetParentId: string | null) => { |
| if (dirId === targetParentId) return; |
| const res = await updateKnowledgeDirectory(localStorage.token, knowledge.id, dirId, { |
| parent_id: targetParentId |
| }).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (res) { |
| toast.success($i18n.t('Directory moved.')); |
| getItemsPage(); |
| } |
| }; |
| |
| const deleteFileHandler = async (fileId) => { |
| try { |
| console.log('Starting file deletion process for:', fileId); |
| |
| // Remove from knowledge base only |
| const res = await removeFileFromKnowledgeById(localStorage.token, id, fileId); |
| console.log('Knowledge base updated:', res); |
| |
| if (res) { |
| toast.success($i18n.t('File removed successfully.')); |
| await init(); |
| } |
| } catch (e) { |
| console.error('Error in deleteFileHandler:', e); |
| toast.error(`${e}`); |
| } |
| }; |
| |
| const renameFileHandler = async (fileId: string, name: string) => { |
| try { |
| const res = await renameFileById(localStorage.token, fileId, name); |
| if (res) { |
| toast.success($i18n.t('File renamed.')); |
| getItemsPage(); |
| } |
| } catch (e) { |
| toast.error(`${e}`); |
| } |
| }; |
| |
| let debounceTimeout = null; |
| let mediaQuery; |
| |
| let dragged = false; |
| let isSaving = false; |
| |
| const updateFileContentHandler = async () => { |
| if (isSaving) { |
| console.log('Save operation already in progress, skipping...'); |
| return; |
| } |
| |
| isSaving = true; |
| |
| try { |
| const res = await updateFileDataContentById( |
| localStorage.token, |
| selectedFile.id, |
| selectedFileContent |
| ).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (res) { |
| toast.success($i18n.t('File content updated successfully.')); |
| |
| selectedFileId = null; |
| selectedFile = null; |
| selectedFileContent = ''; |
| |
| await init(); |
| } |
| } finally { |
| isSaving = false; |
| } |
| }; |
| |
| const changeDebounceHandler = () => { |
| console.log('debounce'); |
| if (debounceTimeout) { |
| clearTimeout(debounceTimeout); |
| } |
| |
| debounceTimeout = setTimeout(async () => { |
| if (knowledge.name.trim() === '' || knowledge.description.trim() === '') { |
| toast.error($i18n.t('Please fill in all fields.')); |
| return; |
| } |
| |
| const res = await updateKnowledgeById(localStorage.token, id, { |
| ...knowledge, |
| name: knowledge.name, |
| description: knowledge.description, |
| access_grants: knowledge.access_grants ?? [] |
| }).catch((e) => { |
| toast.error(`${e}`); |
| }); |
| |
| if (res) { |
| toast.success($i18n.t('Knowledge updated successfully')); |
| } |
| }, 1000); |
| }; |
| |
| const handleMediaQuery = async (e) => { |
| if (e.matches) { |
| largeScreen = true; |
| } else { |
| largeScreen = false; |
| } |
| }; |
| |
| const onDragOver = (e) => { |
| e.preventDefault(); |
| |
| // Check if a file is being draggedOver. |
| if (e.dataTransfer?.types?.includes('Files')) { |
| dragged = true; |
| } else { |
| dragged = false; |
| } |
| }; |
| |
| const onDragLeave = () => { |
| dragged = false; |
| }; |
| |
| const onDrop = async (e) => { |
| e.preventDefault(); |
| dragged = false; |
| |
| if (!knowledge?.write_access) { |
| toast.error($i18n.t('You do not have permission to upload files to this knowledge base.')); |
| return; |
| } |
| |
| const handleUploadingFileFolder = (items) => { |
| for (const item of items) { |
| if (item.isFile) { |
| item.file((file) => { |
| uploadFileHandler(file); |
| }); |
| continue; |
| } |
| |
| |
| const wkentry = item.webkitGetAsEntry(); |
| const isDirectory = wkentry.isDirectory; |
| if (isDirectory) { |
| // Read the directory |
| wkentry.createReader().readEntries( |
| (entries) => { |
| handleUploadingFileFolder(entries); |
| }, |
| (error) => { |
| console.error('Error reading directory entries:', error); |
| } |
| ); |
| } else { |
| uploadFileHandler(item.getAsFile()); |
| } |
| } |
| }; |
| |
| if (e.dataTransfer?.types?.includes('Files')) { |
| if (e.dataTransfer?.files) { |
| const inputItems = e.dataTransfer?.items; |
| |
| if (inputItems && inputItems.length > 0) { |
| handleUploadingFileFolder(inputItems); |
| } else { |
| toast.error($i18n.t(`File not found.`)); |
| } |
| } |
| } |
| }; |
| |
| onMount(async () => { |
| // listen to resize 1024px |
| mediaQuery = window.matchMedia('(min-width: 1024px)'); |
| |
| mediaQuery.addEventListener('change', handleMediaQuery); |
| handleMediaQuery(mediaQuery); |
| |
| // Select the container element you want to observe |
| const container = document.getElementById('collection-container'); |
| |
| // initialize the minSize based on the container width |
| minSize = !largeScreen ? 100 : Math.floor((300 / container.clientWidth) * 100); |
| |
| // Create a new ResizeObserver instance |
| const resizeObserver = new ResizeObserver((entries) => { |
| for (let entry of entries) { |
| const width = entry.contentRect.width; |
| // calculate the percentage of 300 |
| const percentage = (300 / width) * 100; |
| // set the minSize to the percentage, must be an integer |
| minSize = !largeScreen ? 100 : Math.floor(percentage); |
| |
| if (showSidepanel) { |
| if (pane && pane.isExpanded() && pane.getSize() < minSize) { |
| pane.resize(minSize); |
| } |
| } |
| } |
| }); |
| |
| |
| resizeObserver.observe(container); |
| |
| if (pane) { |
| pane.expand(); |
| } |
| |
| id = $page.params.id; |
| const res = await getKnowledgeById(localStorage.token, id).catch((e) => { |
| toast.error(`${e}`); |
| return null; |
| }); |
| |
| if (res) { |
| knowledge = res; |
| if (!Array.isArray(knowledge?.access_grants)) { |
| knowledge.access_grants = []; |
| } |
| knowledgeId = knowledge?.id; |
| } else { |
| goto('/workspace/knowledge'); |
| } |
| |
| const dropZone = document.querySelector('body'); |
| dropZone?.addEventListener('dragover', onDragOver); |
| dropZone?.addEventListener('drop', onDrop); |
| dropZone?.addEventListener('dragleave', onDragLeave); |
| }); |
| |
| onDestroy(() => { |
| clearTimeout(searchDebounceTimer); |
| if (pendingPollTimer) { |
| clearInterval(pendingPollTimer); |
| pendingPollTimer = null; |
| } |
| mediaQuery?.removeEventListener('change', handleMediaQuery); |
| const dropZone = document.querySelector('body'); |
| dropZone?.removeEventListener('dragover', onDragOver); |
| dropZone?.removeEventListener('drop', onDrop); |
| dropZone?.removeEventListener('dragleave', onDragLeave); |
| }); |
| |
| const decodeString = (str: string) => { |
| try { |
| return decodeURIComponent(str); |
| } catch (e) { |
| return str; |
| } |
| }; |
| </script> |
| |
| <FilesOverlay show={dragged} /> |
| <SyncConfirmDialog |
| bind:show={showSyncConfirmModal} |
| message={$i18n.t( |
| '{{count}} files selected. Only new and modified files will be uploaded. Deleted files will be removed. The folder structure will be mirrored. Continue?', |
| { count: pendingSyncFiles?.length ?? 0 } |
| )} |
| on:confirm={() => { |
| syncDirectoryHandler(); |
| }} |
| on:cancel={() => { |
| pendingSyncFiles = null; |
| }} |
| /> |
| |
| <AttachWebpageModal |
| bind:show={showAddWebpageModal} |
| onSubmit={async (e) => { |
| uploadWeb(e.data); |
| }} |
| /> |
| |
| <AddTextContentModal |
| bind:show={showAddTextContentModal} |
| on:submit={(e) => { |
| const file = createFileFromText(e.detail.name, e.detail.content); |
| uploadFileHandler(file); |
| }} |
| /> |
| |
| <NewDirectoryModal |
| bind:show={showNewDirectoryModal} |
| on:submit={(e) => { |
| createDirectoryHandler(e.detail.name); |
| }} |
| /> |
| |
| <input |
| id="files-input" |
| bind:files={inputFiles} |
| type="file" |
| multiple |
| hidden |
| on:change={async () => { |
| if (inputFiles && inputFiles.length > 0) { |
| for (const file of inputFiles) { |
| await uploadFileHandler(file); |
| } |
| |
| inputFiles = null; |
| const fileInputElement = document.getElementById('files-input'); |
| |
| if (fileInputElement) { |
| fileInputElement.value = ''; |
| } |
| } else { |
| toast.error($i18n.t(`File not found.`)); |
| } |
| }} |
| /> |
| |
| <div class="flex flex-col w-full h-full min-h-full" id="collection-container"> |
| {#if id && knowledge} |
| <AccessControlModal |
| bind:show={showAccessControlModal} |
| bind:accessGrants={knowledge.access_grants} |
| share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'} |
| sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'} |
| shareUsers={($user?.permissions?.access_grants?.allow_users ?? true) || |
| $user?.role === 'admin'} |
| onChange={async () => { |
| try { |
| await updateKnowledgeAccessGrants(localStorage.token, id, knowledge.access_grants ?? []); |
| toast.success($i18n.t('Saved')); |
| } catch (error) { |
| toast.error(`${error}`); |
| } |
| }} |
| accessRoles={['read', 'write']} |
| /> |
| <div class="w-full px-2"> |
| <div class=" flex w-full"> |
| <div class="flex-1"> |
| <div class="flex items-center justify-between w-full"> |
| <div class="w-full flex justify-between items-center"> |
| <input |
| type="text" |
| class="text-left w-full text-lg bg-transparent outline-hidden flex-1" |
| bind:value={knowledge.name} |
| aria-label={$i18n.t('Knowledge Name')} |
| placeholder={$i18n.t('Knowledge Name')} |
| disabled={!knowledge?.write_access} |
| on:input={() => { |
| changeDebounceHandler(); |
| }} |
| /> |
| |
| <div class="shrink-0 mr-2.5"> |
| {#if fileItemsTotal} |
| <div class="text-xs text-gray-500"> |
| {$i18n.t('{{COUNT} |
| {$i18n.t('{{COUNT}} files', { |
| COUNT: fileItemsTotal |
| })} |
| </div> |
| {/if} |
| </div> |
| </div> |
| |
| {#if knowledge?.write_access} |
| <div class="self-center shrink-0"> |
| <button |
| class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center" |
| type="button" |
| on:click={() => { |
| showAccessControlModal = true; |
| }} |
| > |
| <LockClosed strokeWidth="2.5" className="size-3.5" /> |
| |
| <div class="text-sm font-medium shrink-0"> |
| {$i18n.t('Access')} |
| </div> |
| </button> |
| </div> |
| {:else} |
| <div class="text-xs shrink-0 text-gray-500"> |
| {$i18n.t('Read Only')} |
| </div> |
| {/if} |
| </div> |
| |
| <div class="flex w-full items-center"> |
| <input |
| type="text" |
| class="text-left text-xs w-full text-gray-500 bg-transparent outline-hidden flex-1" |
| bind:value={knowledge.description} |
| aria-label={$i18n.t('Knowledge Description')} |
| placeholder={$i18n.t('Knowledge Description')} |
| disabled={!knowledge?.write_access} |
| on:input={() => { |
| changeDebounceHandler(); |
| }} |
| /> |
| |
| <div class="hidden md:block"> |
| <Tooltip content={$i18n.t('Click to copy ID')}> |
| <button |
| class="text-xs text-gray-500 font-mono shrink-0 px-2 py-1 rounded-lg cursor-pointer hover:underline transition whitespace-nowrap" |
| on:click={() => { |
| copyToClipboard(id); |
| toast.success($i18n.t('ID copied to clipboard')); |
| }} |
| > |
| {id} |
| </button> |
| </Tooltip> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div |
| class="mt-2 mb-2.5 py-2 -mx-0 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100/30 dark:border-gray-850/30 flex-1" |
| > |
| <div class="px-3.5 flex flex-1 items-center w-full space-x-2 py-0.5 pb-2"> |
| <div class="flex flex-1 items-center"> |
| <div class=" self-center ml-1 mr-3"> |
| <Search className="size-3.5" /> |
| </div> |
| <input |
| class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent" |
| bind:value={query} |
| aria-label={$i18n.t('Search Collection')} |
| placeholder={$i18n.t('Search Collection')} |
| on:focus={() => { |
| selectedFileId = null; |
| }} |
| /> |
| |
| <Dropdown align="end"> |
| <button |
| class="p-1.5 mr-1 rounded-xl text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition" |
| type="button" |
| > |
| <AdjustmentsHorizontal className="size-3.5" strokeWidth="2" /> |
| </button> |
| |
| <div slot="content"> |
| <div |
| class="min-w-[180px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg" |
| > |
| <button |
| class="select-none flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full" |
| type="button" |
| on:click={() => { |
| includeContent = !includeContent; |
| }} |
| > |
| <Checkbox |
| state={includeContent ? 'checked' : 'unchecked'} |
| on:change={(e) => { |
| includeContent = e.detail === 'checked'; |
| }} |
| /> |
| {$i18n.t('File content')} |
| </button> |
| </div> |
| </div> |
| </Dropdown> |
| |
| {#if knowledge?.write_access} |
| <div> |
| <AddContentMenu |
| onUpload={(data) => { |
| if (data.type === 'directory') { |
| uploadDirectoryHandler(); |
| } else if (data.type === 'new_directory') { |
| showNewDirectoryModal = true; |
| } else if (data.type === 'web') { |
| showAddWebpageModal = true; |
| } else if (data.type === 'text') { |
| showAddTextContentModal = true; |
| } else { |
| document.getElementById('files-input').click(); |
| } |
| }} |
| onSync={async () => { |
| pendingSyncFiles = await collectDirectoryFiles(); |
| if (pendingSyncFiles?.length) { |
| showSyncConfirmModal = true; |
| } |
| }} |
| onReset={() => { |
| showResetConfirm = true; |
| }} |
| /> |
| </div> |
| {/if} |
| </div> |
| </div> |
| |
| <div class="px-3 flex justify-between"> |
| <div |
| class="flex w-full bg-transparent overflow-x-auto scrollbar-none" |
| on:wheel={(e) => { |
| if (e.deltaY !== 0) { |
| e.preventDefault(); |
| e.currentTarget.scrollLeft += e.deltaY; |
| } |
| }} |
| > |
| <div |
| class="flex gap-3 w-fit text-center text-sm rounded-full bg-transparent px-0.5 whitespace-nowrap" |
| > |
| <DropdownOptions |
| align="start" |
| className="flex shrink-0 items-center gap-2 px-3 py-1.5 text-sm bg-gray-50 dark:bg-gray-850 rounded-xl placeholder-gray-400 outline-hidden focus:outline-hidden" |
| bind:value={viewOption} |
| items={[ |
| { value: null, label: $i18n.t('All') }, |
| { value: 'created', label: $i18n.t('Created by you') }, |
| { value: 'shared', label: $i18n.t('Shared with you') } |
| ]} |
| onChange={(value) => { |
| if (value) { |
| localStorage.workspaceViewOption = value; |
| } else { |
| delete localStorage.workspaceViewOption; |
| } |
| }} |
| /> |
| |
| <DropdownOptions |
| align="start" |
| bind:value={sortKey} |
| placeholder={$i18n.t('Sort')} |
| items={[ |
| { value: 'name', label: $i18n.t('Name') }, |
| { value: 'created_at', label: $i18n.t('Created At') }, |
| { value: 'updated_at', label: $i18n.t('Updated At') } |
| ]} |
| /> |
| |
| {#if sortKey} |
| <DropdownOptions |
| align="start" |
| bind:value={direction} |
| items={[ |
| { value: 'asc', label: $i18n.t('Asc') }, |
| { value: null, label: $i18n.t('Desc') } |
| ]} |
| /> |
| {/if} |
| </div> |
| </div> |
| </div> |
| |
| {#if currentDirectoryId !== null} |
| <div class="px-5 mt-2"> |
| <KnowledgeBreadcrumbs |
| rootLabel={knowledge.name} |
| {breadcrumbs} |
| onNavigate={(dirId) => navigateToDirectory(dirId)} |
| onMoveFile={(fileId, dirId) => moveFileToDirectoryHandler(fileId, dirId)} |
| onMoveDir={(dirId, targetId) => moveDirectoryHandler(dirId, targetId)} |
| /> |
| </div> |
| {/if} |
| |
| {#if syncing} |
| <div class="mx-2.5 mt-2.5 -mb-0.5"> |
| <div class="flex items-center gap-2.5 rounded-xl py-2 px-3 bg-gray-50 dark:bg-gray-850"> |
| <Spinner className="size-3.5 shrink-0" /> |
| <div class="text-xs text-gray-500 dark:text-gray-400 truncate"> |
| {syncing} |
| </div> |
| </div> |
| </div> |
| {/if} |
| |
| {#if fileItems !== null && fileItemsTotal !== null} |
| <div class="flex flex-row flex-1 gap-3 px-2.5 mt-2"> |
| <div class="flex-1 flex"> |
| <div class=" flex flex-col w-full space-x-2 rounded-lg h-full"> |
| <div class="w-full h-full flex flex-col min-h-full"> |
| {#if fileItems.length > 0 || directoryItems.length > 0} |
| <div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs"> |
| <Files |
| files={fileItems} |
| directories={directoryItems} |
| {knowledge} |
| {selectedFileId} |
| onClick={(fileId) => { |
| selectedFileId = fileId; |
| |
| if (fileItems) { |
| const file = fileItems.find((file) => file.id === selectedFileId); |
| if (file) { |
| fileSelectHandler(file); |
| } else { |
| selectedFile = null; |
| } |
| } |
| }} |
| onDelete={(fileId) => { |
| selectedFileId = null; |
| selectedFile = null; |
| |
| deleteFileHandler(fileId); |
| }} |
| onRename={(fileId, name) => renameFileHandler(fileId, name)} |
| onNavigateDirectory={(dirId) => navigateToDirectory(dirId)} |
| onRenameDirectory={(id, name) => renameDirectoryHandler(id, name)} |
| onDeleteDirectory={(id) => confirmDeleteDirectory(id)} |
| onMoveFileToDirectory={(fileId, dirId) => |
| moveFileToDirectoryHandler(fileId, dirId)} |
| onMoveDirectoryToDirectory={(dirId, targetId) => |
| moveDirectoryHandler(dirId, targetId)} |
| /> |
| </div> |
| |
| {#if fileItemsTotal > 30} |
| <Pagination bind:page={currentPage} count={fileItemsTotal} perPage={30} /> |
| {/if} |
| {:else} |
| <div class="my-3 flex flex-col justify-center text-center text-gray-500 text-xs"> |
| <div> |
| {$i18n.t('No content found')} |
| </div> |
| </div> |
| {/if} |
| </div> |
| </div> |
| </div> |
| |
| {#if selectedFileId !== null} |
| <Drawer |
| className="h-full" |
| show={selectedFileId !== null} |
| onClose={() => { |
| selectedFileId = null; |
| selectedFile = null; |
| }} |
| > |
| <div class="flex flex-col justify-start h-full max-h-full"> |
| <div class=" flex flex-col w-full h-full max-h-full"> |
| <div class="shrink-0 flex items-center p-2"> |
| <div class="mr-2"> |
| <button |
| class="w-full text-left text-sm p-1.5 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850" |
| aria-label={$i18n.t('Close')} |
| on:click={() => { |
| selectedFileId = null; |
| selectedFile = null; |
| }} |
| > |
| <ChevronLeft strokeWidth="2.5" /> |
| </button> |
| </div> |
| <div class=" flex-1 text-lg line-clamp-1"> |
| {selectedFile?.meta?.name} |
| </div> |
| |
| {#if knowledge?.write_access} |
| <div> |
| <button |
| class="flex self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" |
| disabled={isSaving} |
| on:click={() => { |
| updateFileContentHandler(); |
| }} |
| > |
| {$i18n.t('Save')} |
| {#if isSaving} |
| <div class="ml-2 self-center"> |
| <Spinner /> |
| </div> |
| {/if} |
| </button> |
| </div> |
| {/if} |
| </div> |
| |
| {#key selectedFile.id} |
| <textarea |
| class="w-full h-full text-sm outline-none resize-none px-3 py-2" |
| bind:value={selectedFileContent} |
| disabled={!knowledge?.write_access} |
| aria-label={$i18n.t('File content')} |
| placeholder={$i18n.t('Add content here')} |
| /> |
| {/key} |
| </div> |
| </div> |
| </Drawer> |
| {/if} |
| </div> |
| {:else} |
| <div class="my-10"> |
| <Spinner className="size-4" /> |
| </div> |
| {/if} |
| </div> |
| {:else} |
| <Spinner className="size-5" /> |
| {/if} |
| </div> |
| |
| <ConfirmDialog |
| bind:show={showDeleteDirectoryConfirm} |
| title={$i18n.t('Delete directory?')} |
| on:confirm={() => { |
| deleteDirectoryHandler(!deleteDirectoryContents); |
| }} |
| on:cancel={() => { |
| pendingDeleteDirectoryId = null; |
| }} |
| > |
| <div class="text-sm text-gray-700 dark:text-gray-300 flex-1 line-clamp-3 mb-2"> |
| {$i18n.t(`Are you sure you want to delete this directory?`)} |
| </div> |
| |
| <div class="flex items-center gap-1.5"> |
| <input type="checkbox" bind:checked={deleteDirectoryContents} /> |
| |
| <div class="text-xs text-gray-500"> |
| {$i18n.t('Delete all contents inside this directory')} |
| </div> |
| </div> |
| </ConfirmDialog> |
| |
| <ConfirmDialog |
| bind:show={showResetConfirm} |
| title={$i18n.t('Reset knowledge base?')} |
| on:confirm={async () => { |
| await resetKnowledgeById(localStorage.token, id); |
| toast.success($i18n.t('Knowledge base has been reset')); |
| init(); |
| }} |
| > |
| <div class="text-sm text-gray-700 dark:text-gray-300 flex-1 line-clamp-3"> |
| {$i18n.t( |
| 'This will remove all files and directories from this knowledge base. This action cannot be undone.' |
| )} |
| </div> |
| </ConfirmDialog> |
| |