KnowledgeBase.svelte 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887
  1. <script lang="ts">
  2. import Fuse from 'fuse.js';
  3. import { toast } from 'svelte-sonner';
  4. import { v4 as uuidv4 } from 'uuid';
  5. import { PaneGroup, Pane, PaneResizer } from 'paneforge';
  6. import { onMount, getContext, onDestroy, tick } from 'svelte';
  7. const i18n = getContext('i18n');
  8. import { goto } from '$app/navigation';
  9. import { page } from '$app/stores';
  10. import { mobile, showSidebar, knowledge as _knowledge, config, user } from '$lib/stores';
  11. import { updateFileDataContentById, uploadFile, deleteFileById } from '$lib/apis/files';
  12. import {
  13. addFileToKnowledgeById,
  14. getKnowledgeById,
  15. getKnowledgeBases,
  16. removeFileFromKnowledgeById,
  17. resetKnowledgeById,
  18. updateFileFromKnowledgeById,
  19. updateKnowledgeById
  20. } from '$lib/apis/knowledge';
  21. import { transcribeAudio } from '$lib/apis/audio';
  22. import { blobToFile } from '$lib/utils';
  23. import { processFile } from '$lib/apis/retrieval';
  24. import Spinner from '$lib/components/common/Spinner.svelte';
  25. import Files from './KnowledgeBase/Files.svelte';
  26. import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
  27. import AddContentMenu from './KnowledgeBase/AddContentMenu.svelte';
  28. import AddTextContentModal from './KnowledgeBase/AddTextContentModal.svelte';
  29. import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
  30. import RichTextInput from '$lib/components/common/RichTextInput.svelte';
  31. import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
  32. import Drawer from '$lib/components/common/Drawer.svelte';
  33. import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
  34. import LockClosed from '$lib/components/icons/LockClosed.svelte';
  35. import AccessControlModal from '../common/AccessControlModal.svelte';
  36. let largeScreen = true;
  37. let pane;
  38. let showSidepanel = true;
  39. let minSize = 0;
  40. type Knowledge = {
  41. id: string;
  42. name: string;
  43. description: string;
  44. data: {
  45. file_ids: string[];
  46. };
  47. files: any[];
  48. };
  49. let id = null;
  50. let knowledge: Knowledge | null = null;
  51. let query = '';
  52. let showAddTextContentModal = false;
  53. let showSyncConfirmModal = false;
  54. let showAccessControlModal = false;
  55. let inputFiles = null;
  56. let filteredItems = [];
  57. $: if (knowledge && knowledge.files) {
  58. fuse = new Fuse(knowledge.files, {
  59. keys: ['meta.name', 'meta.description']
  60. });
  61. }
  62. $: if (fuse) {
  63. filteredItems = query
  64. ? fuse.search(query).map((e) => {
  65. return e.item;
  66. })
  67. : (knowledge?.files ?? []);
  68. }
  69. let selectedFile = null;
  70. let selectedFileId = null;
  71. $: if (selectedFileId) {
  72. const file = (knowledge?.files ?? []).find((file) => file.id === selectedFileId);
  73. if (file) {
  74. file.data = file.data ?? { content: '' };
  75. selectedFile = file;
  76. } else {
  77. selectedFile = null;
  78. }
  79. } else {
  80. selectedFile = null;
  81. }
  82. let fuse = null;
  83. let debounceTimeout = null;
  84. let mediaQuery;
  85. let dragged = false;
  86. const createFileFromText = (name, content) => {
  87. const blob = new Blob([content], { type: 'text/plain' });
  88. const file = blobToFile(blob, `${name}.txt`);
  89. console.log(file);
  90. return file;
  91. };
  92. const uploadFileHandler = async (file) => {
  93. console.log(file);
  94. const tempItemId = uuidv4();
  95. const fileItem = {
  96. type: 'file',
  97. file: '',
  98. id: null,
  99. url: '',
  100. name: file.name,
  101. size: file.size,
  102. status: 'uploading',
  103. error: '',
  104. itemId: tempItemId
  105. };
  106. if (fileItem.size == 0) {
  107. toast.error($i18n.t('You cannot upload an empty file.'));
  108. return null;
  109. }
  110. if (
  111. ($config?.file?.max_size ?? null) !== null &&
  112. file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
  113. ) {
  114. console.log('File exceeds max size limit:', {
  115. fileSize: file.size,
  116. maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
  117. });
  118. toast.error(
  119. $i18n.t(`File size should not exceed {{maxSize}} MB.`, {
  120. maxSize: $config?.file?.max_size
  121. })
  122. );
  123. return;
  124. }
  125. knowledge.files = [...(knowledge.files ?? []), fileItem];
  126. try {
  127. const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => {
  128. toast.error(`${e}`);
  129. return null;
  130. });
  131. if (uploadedFile) {
  132. console.log(uploadedFile);
  133. knowledge.files = knowledge.files.map((item) => {
  134. if (item.itemId === tempItemId) {
  135. item.id = uploadedFile.id;
  136. }
  137. // Remove temporary item id
  138. delete item.itemId;
  139. return item;
  140. });
  141. await addFileHandler(uploadedFile.id);
  142. } else {
  143. toast.error($i18n.t('Failed to upload file.'));
  144. }
  145. } catch (e) {
  146. toast.error(`${e}`);
  147. }
  148. };
  149. const uploadDirectoryHandler = async () => {
  150. // Check if File System Access API is supported
  151. const isFileSystemAccessSupported = 'showDirectoryPicker' in window;
  152. try {
  153. if (isFileSystemAccessSupported) {
  154. // Modern browsers (Chrome, Edge) implementation
  155. await handleModernBrowserUpload();
  156. } else {
  157. // Firefox fallback
  158. await handleFirefoxUpload();
  159. }
  160. } catch (error) {
  161. handleUploadError(error);
  162. }
  163. };
  164. // Helper function to check if a path contains hidden folders
  165. const hasHiddenFolder = (path) => {
  166. return path.split('/').some((part) => part.startsWith('.'));
  167. };
  168. // Modern browsers implementation using File System Access API
  169. const handleModernBrowserUpload = async () => {
  170. const dirHandle = await window.showDirectoryPicker();
  171. let totalFiles = 0;
  172. let uploadedFiles = 0;
  173. // Function to update the UI with the progress
  174. const updateProgress = () => {
  175. const percentage = (uploadedFiles / totalFiles) * 100;
  176. toast.info(`Upload Progress: ${uploadedFiles}/${totalFiles} (${percentage.toFixed(2)}%)`);
  177. };
  178. // Recursive function to count all files excluding hidden ones
  179. async function countFiles(dirHandle) {
  180. for await (const entry of dirHandle.values()) {
  181. // Skip hidden files and directories
  182. if (entry.name.startsWith('.')) continue;
  183. if (entry.kind === 'file') {
  184. totalFiles++;
  185. } else if (entry.kind === 'directory') {
  186. // Only process non-hidden directories
  187. if (!entry.name.startsWith('.')) {
  188. await countFiles(entry);
  189. }
  190. }
  191. }
  192. }
  193. // Recursive function to process directories excluding hidden files and folders
  194. async function processDirectory(dirHandle, path = '') {
  195. for await (const entry of dirHandle.values()) {
  196. // Skip hidden files and directories
  197. if (entry.name.startsWith('.')) continue;
  198. const entryPath = path ? `${path}/${entry.name}` : entry.name;
  199. // Skip if the path contains any hidden folders
  200. if (hasHiddenFolder(entryPath)) continue;
  201. if (entry.kind === 'file') {
  202. const file = await entry.getFile();
  203. const fileWithPath = new File([file], entryPath, { type: file.type });
  204. await uploadFileHandler(fileWithPath);
  205. uploadedFiles++;
  206. updateProgress();
  207. } else if (entry.kind === 'directory') {
  208. // Only process non-hidden directories
  209. if (!entry.name.startsWith('.')) {
  210. await processDirectory(entry, entryPath);
  211. }
  212. }
  213. }
  214. }
  215. await countFiles(dirHandle);
  216. updateProgress();
  217. if (totalFiles > 0) {
  218. await processDirectory(dirHandle);
  219. } else {
  220. console.log('No files to upload.');
  221. }
  222. };
  223. // Firefox fallback implementation using traditional file input
  224. const handleFirefoxUpload = async () => {
  225. return new Promise((resolve, reject) => {
  226. // Create hidden file input
  227. const input = document.createElement('input');
  228. input.type = 'file';
  229. input.webkitdirectory = true;
  230. input.directory = true;
  231. input.multiple = true;
  232. input.style.display = 'none';
  233. // Add input to DOM temporarily
  234. document.body.appendChild(input);
  235. input.onchange = async () => {
  236. try {
  237. const files = Array.from(input.files)
  238. // Filter out files from hidden folders
  239. .filter((file) => !hasHiddenFolder(file.webkitRelativePath));
  240. let totalFiles = files.length;
  241. let uploadedFiles = 0;
  242. // Function to update the UI with the progress
  243. const updateProgress = () => {
  244. const percentage = (uploadedFiles / totalFiles) * 100;
  245. toast.info(
  246. `Upload Progress: ${uploadedFiles}/${totalFiles} (${percentage.toFixed(2)}%)`
  247. );
  248. };
  249. updateProgress();
  250. // Process all files
  251. for (const file of files) {
  252. // Skip hidden files (additional check)
  253. if (!file.name.startsWith('.')) {
  254. const relativePath = file.webkitRelativePath || file.name;
  255. const fileWithPath = new File([file], relativePath, { type: file.type });
  256. await uploadFileHandler(fileWithPath);
  257. uploadedFiles++;
  258. updateProgress();
  259. }
  260. }
  261. // Clean up
  262. document.body.removeChild(input);
  263. resolve();
  264. } catch (error) {
  265. reject(error);
  266. }
  267. };
  268. input.onerror = (error) => {
  269. document.body.removeChild(input);
  270. reject(error);
  271. };
  272. // Trigger file picker
  273. input.click();
  274. });
  275. };
  276. // Error handler
  277. const handleUploadError = (error) => {
  278. if (error.name === 'AbortError') {
  279. toast.info('Directory selection was cancelled');
  280. } else {
  281. toast.error('Error accessing directory');
  282. console.error('Directory access error:', error);
  283. }
  284. };
  285. // Helper function to maintain file paths within zip
  286. const syncDirectoryHandler = async () => {
  287. if ((knowledge?.files ?? []).length > 0) {
  288. const res = await resetKnowledgeById(localStorage.token, id).catch((e) => {
  289. toast.error(`${e}`);
  290. });
  291. if (res) {
  292. knowledge = res;
  293. toast.success($i18n.t('Knowledge reset successfully.'));
  294. // Upload directory
  295. uploadDirectoryHandler();
  296. }
  297. } else {
  298. uploadDirectoryHandler();
  299. }
  300. };
  301. const addFileHandler = async (fileId) => {
  302. const updatedKnowledge = await addFileToKnowledgeById(localStorage.token, id, fileId).catch(
  303. (e) => {
  304. toast.error(`${e}`);
  305. return null;
  306. }
  307. );
  308. if (updatedKnowledge) {
  309. knowledge = updatedKnowledge;
  310. toast.success($i18n.t('File added successfully.'));
  311. } else {
  312. toast.error($i18n.t('Failed to add file.'));
  313. knowledge.files = knowledge.files.filter((file) => file.id !== fileId);
  314. }
  315. };
  316. const deleteFileHandler = async (fileId) => {
  317. try {
  318. console.log('Starting file deletion process for:', fileId);
  319. // Remove from knowledge base only
  320. const updatedKnowledge = await removeFileFromKnowledgeById(localStorage.token, id, fileId);
  321. console.log('Knowledge base updated:', updatedKnowledge);
  322. if (updatedKnowledge) {
  323. knowledge = updatedKnowledge;
  324. toast.success($i18n.t('File removed successfully.'));
  325. }
  326. } catch (e) {
  327. console.error('Error in deleteFileHandler:', e);
  328. toast.error(`${e}`);
  329. }
  330. };
  331. const updateFileContentHandler = async () => {
  332. const fileId = selectedFile.id;
  333. const content = selectedFile.data.content;
  334. const res = updateFileDataContentById(localStorage.token, fileId, content).catch((e) => {
  335. toast.error(`${e}`);
  336. });
  337. const updatedKnowledge = await updateFileFromKnowledgeById(
  338. localStorage.token,
  339. id,
  340. fileId
  341. ).catch((e) => {
  342. toast.error(`${e}`);
  343. });
  344. if (res && updatedKnowledge) {
  345. knowledge = updatedKnowledge;
  346. toast.success($i18n.t('File content updated successfully.'));
  347. }
  348. };
  349. const changeDebounceHandler = () => {
  350. console.log('debounce');
  351. if (debounceTimeout) {
  352. clearTimeout(debounceTimeout);
  353. }
  354. debounceTimeout = setTimeout(async () => {
  355. if (knowledge.name.trim() === '' || knowledge.description.trim() === '') {
  356. toast.error($i18n.t('Please fill in all fields.'));
  357. return;
  358. }
  359. const res = await updateKnowledgeById(localStorage.token, id, {
  360. ...knowledge,
  361. name: knowledge.name,
  362. description: knowledge.description,
  363. access_control: knowledge.access_control
  364. }).catch((e) => {
  365. toast.error(`${e}`);
  366. });
  367. if (res) {
  368. toast.success($i18n.t('Knowledge updated successfully'));
  369. _knowledge.set(await getKnowledgeBases(localStorage.token));
  370. }
  371. }, 1000);
  372. };
  373. const handleMediaQuery = async (e) => {
  374. if (e.matches) {
  375. largeScreen = true;
  376. } else {
  377. largeScreen = false;
  378. }
  379. };
  380. const onDragOver = (e) => {
  381. e.preventDefault();
  382. // Check if a file is being draggedOver.
  383. if (e.dataTransfer?.types?.includes('Files')) {
  384. dragged = true;
  385. } else {
  386. dragged = false;
  387. }
  388. };
  389. const onDragLeave = () => {
  390. dragged = false;
  391. };
  392. const onDrop = async (e) => {
  393. e.preventDefault();
  394. dragged = false;
  395. if (e.dataTransfer?.types?.includes('Files')) {
  396. if (e.dataTransfer?.files) {
  397. const inputFiles = e.dataTransfer?.files;
  398. if (inputFiles && inputFiles.length > 0) {
  399. for (const file of inputFiles) {
  400. await uploadFileHandler(file);
  401. }
  402. } else {
  403. toast.error($i18n.t(`File not found.`));
  404. }
  405. }
  406. }
  407. };
  408. onMount(async () => {
  409. // listen to resize 1024px
  410. mediaQuery = window.matchMedia('(min-width: 1024px)');
  411. mediaQuery.addEventListener('change', handleMediaQuery);
  412. handleMediaQuery(mediaQuery);
  413. // Select the container element you want to observe
  414. const container = document.getElementById('collection-container');
  415. // initialize the minSize based on the container width
  416. minSize = !largeScreen ? 100 : Math.floor((300 / container.clientWidth) * 100);
  417. // Create a new ResizeObserver instance
  418. const resizeObserver = new ResizeObserver((entries) => {
  419. for (let entry of entries) {
  420. const width = entry.contentRect.width;
  421. // calculate the percentage of 300
  422. const percentage = (300 / width) * 100;
  423. // set the minSize to the percentage, must be an integer
  424. minSize = !largeScreen ? 100 : Math.floor(percentage);
  425. if (showSidepanel) {
  426. if (pane && pane.isExpanded() && pane.getSize() < minSize) {
  427. pane.resize(minSize);
  428. }
  429. }
  430. }
  431. });
  432. // Start observing the container's size changes
  433. resizeObserver.observe(container);
  434. if (pane) {
  435. pane.expand();
  436. }
  437. id = $page.params.id;
  438. const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
  439. toast.error(`${e}`);
  440. return null;
  441. });
  442. if (res) {
  443. knowledge = res;
  444. } else {
  445. goto('/workspace/knowledge');
  446. }
  447. const dropZone = document.querySelector('body');
  448. dropZone?.addEventListener('dragover', onDragOver);
  449. dropZone?.addEventListener('drop', onDrop);
  450. dropZone?.addEventListener('dragleave', onDragLeave);
  451. });
  452. onDestroy(() => {
  453. mediaQuery?.removeEventListener('change', handleMediaQuery);
  454. const dropZone = document.querySelector('body');
  455. dropZone?.removeEventListener('dragover', onDragOver);
  456. dropZone?.removeEventListener('drop', onDrop);
  457. dropZone?.removeEventListener('dragleave', onDragLeave);
  458. });
  459. const decodeString = (str: string) => {
  460. try {
  461. return decodeURIComponent(str);
  462. } catch (e) {
  463. return str;
  464. }
  465. };
  466. </script>
  467. {#if dragged}
  468. <div
  469. class="fixed {$showSidebar
  470. ? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
  471. : 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
  472. id="dropzone"
  473. role="region"
  474. aria-label="Drag and Drop Container"
  475. >
  476. <div class="absolute w-full h-full backdrop-blur-sm bg-gray-800/40 flex justify-center">
  477. <div class="m-auto pt-64 flex flex-col justify-center">
  478. <div class="max-w-md">
  479. <AddFilesPlaceholder>
  480. <div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
  481. Drop any files here to add to my documents
  482. </div>
  483. </AddFilesPlaceholder>
  484. </div>
  485. </div>
  486. </div>
  487. </div>
  488. {/if}
  489. <SyncConfirmDialog
  490. bind:show={showSyncConfirmModal}
  491. message={$i18n.t(
  492. 'This will reset the knowledge base and sync all files. Do you wish to continue?'
  493. )}
  494. on:confirm={() => {
  495. syncDirectoryHandler();
  496. }}
  497. />
  498. <AddTextContentModal
  499. bind:show={showAddTextContentModal}
  500. on:submit={(e) => {
  501. const file = createFileFromText(e.detail.name, e.detail.content);
  502. uploadFileHandler(file);
  503. }}
  504. />
  505. <input
  506. id="files-input"
  507. bind:files={inputFiles}
  508. type="file"
  509. multiple
  510. hidden
  511. on:change={async () => {
  512. if (inputFiles && inputFiles.length > 0) {
  513. for (const file of inputFiles) {
  514. await uploadFileHandler(file);
  515. }
  516. inputFiles = null;
  517. const fileInputElement = document.getElementById('files-input');
  518. if (fileInputElement) {
  519. fileInputElement.value = '';
  520. }
  521. } else {
  522. toast.error($i18n.t(`File not found.`));
  523. }
  524. }}
  525. />
  526. <div class="flex flex-col w-full translate-y-1" id="collection-container">
  527. {#if id && knowledge}
  528. <AccessControlModal
  529. bind:show={showAccessControlModal}
  530. bind:accessControl={knowledge.access_control}
  531. allowPublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
  532. onChange={() => {
  533. changeDebounceHandler();
  534. }}
  535. accessRoles={['read', 'write']}
  536. />
  537. <div class="w-full mb-2.5">
  538. <div class=" flex w-full">
  539. <div class="flex-1">
  540. <div class="flex items-center justify-between w-full px-0.5 mb-1">
  541. <div class="w-full">
  542. <input
  543. type="text"
  544. class="text-left w-full font-semibold text-2xl font-primary bg-transparent outline-hidden"
  545. bind:value={knowledge.name}
  546. placeholder="Knowledge Name"
  547. on:input={() => {
  548. changeDebounceHandler();
  549. }}
  550. />
  551. </div>
  552. <div class="self-center shrink-0">
  553. <button
  554. 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"
  555. type="button"
  556. on:click={() => {
  557. showAccessControlModal = true;
  558. }}
  559. >
  560. <LockClosed strokeWidth="2.5" className="size-3.5" />
  561. <div class="text-sm font-medium shrink-0">
  562. {$i18n.t('Access')}
  563. </div>
  564. </button>
  565. </div>
  566. </div>
  567. <div class="flex w-full px-1">
  568. <input
  569. type="text"
  570. class="text-left text-xs w-full text-gray-500 bg-transparent outline-hidden"
  571. bind:value={knowledge.description}
  572. placeholder="Knowledge Description"
  573. on:input={() => {
  574. changeDebounceHandler();
  575. }}
  576. />
  577. </div>
  578. </div>
  579. </div>
  580. </div>
  581. <div class="flex flex-row flex-1 h-full max-h-full pb-2.5 gap-3">
  582. {#if largeScreen}
  583. <div class="flex-1 flex justify-start w-full h-full max-h-full">
  584. {#if selectedFile}
  585. <div class=" flex flex-col w-full h-full max-h-full">
  586. <div class="shrink-0 mb-2 flex items-center">
  587. {#if !showSidepanel}
  588. <div class="-translate-x-2">
  589. <button
  590. 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"
  591. on:click={() => {
  592. pane.expand();
  593. }}
  594. >
  595. <ChevronLeft strokeWidth="2.5" />
  596. </button>
  597. </div>
  598. {/if}
  599. <div class=" flex-1 text-xl font-medium">
  600. <a
  601. class="hover:text-gray-500 dark:hover:text-gray-100 hover:underline grow line-clamp-1"
  602. href={selectedFile.id ? `/api/v1/files/${selectedFile.id}/content` : '#'}
  603. target="_blank"
  604. >
  605. {decodeString(selectedFile?.meta?.name)}
  606. </a>
  607. </div>
  608. <div>
  609. <button
  610. class="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"
  611. on:click={() => {
  612. updateFileContentHandler();
  613. }}
  614. >
  615. {$i18n.t('Save')}
  616. </button>
  617. </div>
  618. </div>
  619. <div
  620. class=" flex-1 w-full h-full max-h-full text-sm bg-transparent outline-hidden overflow-y-auto scrollbar-hidden"
  621. >
  622. {#key selectedFile.id}
  623. <RichTextInput
  624. className="input-prose-sm"
  625. bind:value={selectedFile.data.content}
  626. placeholder={$i18n.t('Add content here')}
  627. preserveBreaks={true}
  628. />
  629. {/key}
  630. </div>
  631. </div>
  632. {:else}
  633. <div class="h-full flex w-full">
  634. <div class="m-auto text-xs text-center text-gray-200 dark:text-gray-700">
  635. {$i18n.t('Drag and drop a file to upload or select a file to view')}
  636. </div>
  637. </div>
  638. {/if}
  639. </div>
  640. {:else if !largeScreen && selectedFileId !== null}
  641. <Drawer
  642. className="h-full"
  643. show={selectedFileId !== null}
  644. on:close={() => {
  645. selectedFileId = null;
  646. }}
  647. >
  648. <div class="flex flex-col justify-start h-full max-h-full p-2">
  649. <div class=" flex flex-col w-full h-full max-h-full">
  650. <div class="shrink-0 mt-1 mb-2 flex items-center">
  651. <div class="mr-2">
  652. <button
  653. 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"
  654. on:click={() => {
  655. selectedFileId = null;
  656. }}
  657. >
  658. <ChevronLeft strokeWidth="2.5" />
  659. </button>
  660. </div>
  661. <div class=" flex-1 text-xl line-clamp-1">
  662. {selectedFile?.meta?.name}
  663. </div>
  664. <div>
  665. <button
  666. class="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"
  667. on:click={() => {
  668. updateFileContentHandler();
  669. }}
  670. >
  671. {$i18n.t('Save')}
  672. </button>
  673. </div>
  674. </div>
  675. <div
  676. class=" flex-1 w-full h-full max-h-full py-2.5 px-3.5 rounded-lg text-sm bg-transparent overflow-y-auto scrollbar-hidden"
  677. >
  678. {#key selectedFile.id}
  679. <RichTextInput
  680. className="input-prose-sm"
  681. bind:value={selectedFile.data.content}
  682. placeholder={$i18n.t('Add content here')}
  683. preserveBreaks={true}
  684. />
  685. {/key}
  686. </div>
  687. </div>
  688. </div>
  689. </Drawer>
  690. {/if}
  691. <div
  692. class="{largeScreen ? 'shrink-0 w-72 max-w-72' : 'flex-1'}
  693. flex
  694. py-2
  695. rounded-2xl
  696. border
  697. border-gray-50
  698. h-full
  699. dark:border-gray-850"
  700. >
  701. <div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
  702. <div class="w-full h-full flex flex-col">
  703. <div class=" px-3">
  704. <div class="flex mb-0.5">
  705. <div class=" self-center ml-1 mr-3">
  706. <svg
  707. xmlns="http://www.w3.org/2000/svg"
  708. viewBox="0 0 20 20"
  709. fill="currentColor"
  710. class="w-4 h-4"
  711. >
  712. <path
  713. fill-rule="evenodd"
  714. d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
  715. clip-rule="evenodd"
  716. />
  717. </svg>
  718. </div>
  719. <input
  720. class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
  721. bind:value={query}
  722. placeholder={$i18n.t('Search Collection')}
  723. on:focus={() => {
  724. selectedFileId = null;
  725. }}
  726. />
  727. <div>
  728. <AddContentMenu
  729. on:upload={(e) => {
  730. if (e.detail.type === 'directory') {
  731. uploadDirectoryHandler();
  732. } else if (e.detail.type === 'text') {
  733. showAddTextContentModal = true;
  734. } else {
  735. document.getElementById('files-input').click();
  736. }
  737. }}
  738. on:sync={(e) => {
  739. showSyncConfirmModal = true;
  740. }}
  741. />
  742. </div>
  743. </div>
  744. </div>
  745. {#if filteredItems.length > 0}
  746. <div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
  747. <Files
  748. small
  749. files={filteredItems}
  750. {selectedFileId}
  751. on:click={(e) => {
  752. selectedFileId = selectedFileId === e.detail ? null : e.detail;
  753. }}
  754. on:delete={(e) => {
  755. console.log(e.detail);
  756. selectedFileId = null;
  757. deleteFileHandler(e.detail);
  758. }}
  759. />
  760. </div>
  761. {:else}
  762. <div class="my-3 flex flex-col justify-center text-center text-gray-500 text-xs">
  763. <div>
  764. {$i18n.t('No content found')}
  765. </div>
  766. </div>
  767. {/if}
  768. </div>
  769. </div>
  770. </div>
  771. </div>
  772. {:else}
  773. <Spinner />
  774. {/if}
  775. </div>