KnowledgeBase.svelte 23 KB

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