KnowledgeBase.svelte 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  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 } 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) {
  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. const updatedKnowledge = await removeFileFromKnowledgeById(
  315. localStorage.token,
  316. id,
  317. fileId
  318. ).catch((e) => {
  319. toast.error(e);
  320. });
  321. if (updatedKnowledge) {
  322. knowledge = updatedKnowledge;
  323. toast.success($i18n.t('File removed successfully.'));
  324. }
  325. };
  326. const updateFileContentHandler = async () => {
  327. const fileId = selectedFile.id;
  328. const content = selectedFile.data.content;
  329. const res = updateFileDataContentById(localStorage.token, fileId, content).catch((e) => {
  330. toast.error(e);
  331. });
  332. const updatedKnowledge = await updateFileFromKnowledgeById(
  333. localStorage.token,
  334. id,
  335. fileId
  336. ).catch((e) => {
  337. toast.error(e);
  338. });
  339. if (res && updatedKnowledge) {
  340. knowledge = updatedKnowledge;
  341. toast.success($i18n.t('File content updated successfully.'));
  342. }
  343. };
  344. const changeDebounceHandler = () => {
  345. console.log('debounce');
  346. if (debounceTimeout) {
  347. clearTimeout(debounceTimeout);
  348. }
  349. debounceTimeout = setTimeout(async () => {
  350. if (knowledge.name.trim() === '' || knowledge.description.trim() === '') {
  351. toast.error($i18n.t('Please fill in all fields.'));
  352. return;
  353. }
  354. const res = await updateKnowledgeById(localStorage.token, id, {
  355. name: knowledge.name,
  356. description: knowledge.description,
  357. access_control: knowledge.access_control
  358. }).catch((e) => {
  359. toast.error(e);
  360. });
  361. if (res) {
  362. toast.success($i18n.t('Knowledge updated successfully'));
  363. _knowledge.set(await getKnowledgeBases(localStorage.token));
  364. }
  365. }, 1000);
  366. };
  367. const handleMediaQuery = async (e) => {
  368. if (e.matches) {
  369. largeScreen = true;
  370. } else {
  371. largeScreen = false;
  372. }
  373. };
  374. const onDragOver = (e) => {
  375. e.preventDefault();
  376. dragged = true;
  377. };
  378. const onDragLeave = () => {
  379. dragged = false;
  380. };
  381. const onDrop = async (e) => {
  382. e.preventDefault();
  383. dragged = false;
  384. if (e.dataTransfer?.files) {
  385. const inputFiles = e.dataTransfer?.files;
  386. if (inputFiles && inputFiles.length > 0) {
  387. for (const file of inputFiles) {
  388. await uploadFileHandler(file);
  389. }
  390. } else {
  391. toast.error($i18n.t(`File not found.`));
  392. }
  393. }
  394. };
  395. onMount(async () => {
  396. // listen to resize 1024px
  397. mediaQuery = window.matchMedia('(min-width: 1024px)');
  398. mediaQuery.addEventListener('change', handleMediaQuery);
  399. handleMediaQuery(mediaQuery);
  400. // Select the container element you want to observe
  401. const container = document.getElementById('collection-container');
  402. // initialize the minSize based on the container width
  403. minSize = !largeScreen ? 100 : Math.floor((300 / container.clientWidth) * 100);
  404. // Create a new ResizeObserver instance
  405. const resizeObserver = new ResizeObserver((entries) => {
  406. for (let entry of entries) {
  407. const width = entry.contentRect.width;
  408. // calculate the percentage of 300
  409. const percentage = (300 / width) * 100;
  410. // set the minSize to the percentage, must be an integer
  411. minSize = !largeScreen ? 100 : Math.floor(percentage);
  412. if (showSidepanel) {
  413. if (pane && pane.isExpanded() && pane.getSize() < minSize) {
  414. pane.resize(minSize);
  415. }
  416. }
  417. }
  418. });
  419. // Start observing the container's size changes
  420. resizeObserver.observe(container);
  421. if (pane) {
  422. pane.expand();
  423. }
  424. id = $page.params.id;
  425. const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
  426. toast.error(e);
  427. return null;
  428. });
  429. if (res) {
  430. knowledge = res;
  431. } else {
  432. goto('/workspace/knowledge');
  433. }
  434. const dropZone = document.querySelector('body');
  435. dropZone?.addEventListener('dragover', onDragOver);
  436. dropZone?.addEventListener('drop', onDrop);
  437. dropZone?.addEventListener('dragleave', onDragLeave);
  438. });
  439. onDestroy(() => {
  440. mediaQuery?.removeEventListener('change', handleMediaQuery);
  441. const dropZone = document.querySelector('body');
  442. dropZone?.removeEventListener('dragover', onDragOver);
  443. dropZone?.removeEventListener('drop', onDrop);
  444. dropZone?.removeEventListener('dragleave', onDragLeave);
  445. });
  446. </script>
  447. {#if dragged}
  448. <div
  449. class="fixed {$showSidebar
  450. ? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
  451. : 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
  452. id="dropzone"
  453. role="region"
  454. aria-label="Drag and Drop Container"
  455. >
  456. <div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
  457. <div class="m-auto pt-64 flex flex-col justify-center">
  458. <div class="max-w-md">
  459. <AddFilesPlaceholder>
  460. <div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
  461. Drop any files here to add to my documents
  462. </div>
  463. </AddFilesPlaceholder>
  464. </div>
  465. </div>
  466. </div>
  467. </div>
  468. {/if}
  469. <SyncConfirmDialog
  470. bind:show={showSyncConfirmModal}
  471. message={$i18n.t(
  472. 'This will reset the knowledge base and sync all files. Do you wish to continue?'
  473. )}
  474. on:confirm={() => {
  475. syncDirectoryHandler();
  476. }}
  477. />
  478. <AddTextContentModal
  479. bind:show={showAddTextContentModal}
  480. on:submit={(e) => {
  481. const file = createFileFromText(e.detail.name, e.detail.content);
  482. uploadFileHandler(file);
  483. }}
  484. />
  485. <input
  486. id="files-input"
  487. bind:files={inputFiles}
  488. type="file"
  489. multiple
  490. hidden
  491. on:change={async () => {
  492. if (inputFiles && inputFiles.length > 0) {
  493. for (const file of inputFiles) {
  494. await uploadFileHandler(file);
  495. }
  496. inputFiles = null;
  497. const fileInputElement = document.getElementById('files-input');
  498. if (fileInputElement) {
  499. fileInputElement.value = '';
  500. }
  501. } else {
  502. toast.error($i18n.t(`File not found.`));
  503. }
  504. }}
  505. />
  506. <div class="flex flex-col w-full h-full max-h-[100dvh] translate-y-1" id="collection-container">
  507. {#if id && knowledge}
  508. <AccessControlModal
  509. bind:show={showAccessControlModal}
  510. bind:accessControl={knowledge.access_control}
  511. onChange={() => {
  512. changeDebounceHandler();
  513. }}
  514. />
  515. <div class="w-full mb-2.5">
  516. <div class=" flex w-full">
  517. <div class="flex-1">
  518. <div class="flex items-center justify-between w-full px-0.5 mb-1">
  519. <div class="w-full">
  520. <input
  521. type="text"
  522. class="text-left w-full font-semibold text-2xl font-primary bg-transparent outline-none"
  523. bind:value={knowledge.name}
  524. placeholder="Knowledge Name"
  525. on:input={() => {
  526. changeDebounceHandler();
  527. }}
  528. />
  529. </div>
  530. <div class="self-center flex-shrink-0">
  531. <button
  532. 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"
  533. type="button"
  534. on:click={() => {
  535. showAccessControlModal = true;
  536. }}
  537. >
  538. <LockClosed strokeWidth="2.5" className="size-3.5" />
  539. <div class="text-sm font-medium flex-shrink-0">
  540. {$i18n.t('Access')}
  541. </div>
  542. </button>
  543. </div>
  544. </div>
  545. <div class="flex w-full px-1">
  546. <input
  547. type="text"
  548. class="text-left text-xs w-full text-gray-500 bg-transparent outline-none"
  549. bind:value={knowledge.description}
  550. placeholder="Knowledge Description"
  551. on:input={() => {
  552. changeDebounceHandler();
  553. }}
  554. />
  555. </div>
  556. </div>
  557. </div>
  558. </div>
  559. <div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
  560. <PaneGroup direction="horizontal">
  561. <Pane
  562. bind:pane
  563. defaultSize={minSize}
  564. collapsible={true}
  565. maxSize={50}
  566. {minSize}
  567. class="h-full"
  568. onExpand={() => {
  569. showSidepanel = true;
  570. }}
  571. onCollapse={() => {
  572. showSidepanel = false;
  573. }}
  574. >
  575. <div
  576. class="{largeScreen ? 'flex-shrink-0' : 'flex-1'}
  577. flex
  578. py-2
  579. rounded-2xl
  580. border
  581. border-gray-50
  582. h-full
  583. dark:border-gray-850"
  584. >
  585. <div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
  586. <div class="w-full h-full flex flex-col">
  587. <div class=" px-3">
  588. <div class="flex py-1">
  589. <div class=" self-center ml-1 mr-3">
  590. <svg
  591. xmlns="http://www.w3.org/2000/svg"
  592. viewBox="0 0 20 20"
  593. fill="currentColor"
  594. class="w-4 h-4"
  595. >
  596. <path
  597. fill-rule="evenodd"
  598. 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"
  599. clip-rule="evenodd"
  600. />
  601. </svg>
  602. </div>
  603. <input
  604. class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
  605. bind:value={query}
  606. placeholder={$i18n.t('Search Collection')}
  607. on:focus={() => {
  608. selectedFileId = null;
  609. }}
  610. />
  611. <div>
  612. <AddContentMenu
  613. on:upload={(e) => {
  614. if (e.detail.type === 'directory') {
  615. uploadDirectoryHandler();
  616. } else if (e.detail.type === 'text') {
  617. showAddTextContentModal = true;
  618. } else {
  619. document.getElementById('files-input').click();
  620. }
  621. }}
  622. on:sync={(e) => {
  623. showSyncConfirmModal = true;
  624. }}
  625. />
  626. </div>
  627. </div>
  628. </div>
  629. {#if filteredItems.length > 0}
  630. <div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
  631. <Files
  632. files={filteredItems}
  633. {selectedFileId}
  634. on:click={(e) => {
  635. selectedFileId = selectedFileId === e.detail ? null : e.detail;
  636. }}
  637. on:delete={(e) => {
  638. console.log(e.detail);
  639. selectedFileId = null;
  640. deleteFileHandler(e.detail);
  641. }}
  642. />
  643. </div>
  644. {:else}
  645. <div
  646. class="m-auto flex flex-col justify-center text-center text-gray-500 text-xs"
  647. >
  648. <div>
  649. {$i18n.t('No content found')}
  650. </div>
  651. <div class="mx-12 mt-2 text-center text-gray-200 dark:text-gray-700">
  652. {$i18n.t('Drag and drop a file to upload or select a file to view')}
  653. </div>
  654. </div>
  655. {/if}
  656. </div>
  657. </div>
  658. </div>
  659. </Pane>
  660. {#if largeScreen}
  661. <PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
  662. <div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
  663. <EllipsisVertical className="size-4 invisible group-hover:visible" />
  664. </div>
  665. </PaneResizer>
  666. <Pane>
  667. <div class="flex-1 flex justify-start h-full max-h-full">
  668. {#if selectedFile}
  669. <div class=" flex flex-col w-full h-full max-h-full ml-2.5">
  670. <div class="flex-shrink-0 mb-2 flex items-center">
  671. {#if !showSidepanel}
  672. <div class="-translate-x-2">
  673. <button
  674. 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"
  675. on:click={() => {
  676. pane.expand();
  677. }}
  678. >
  679. <ChevronLeft strokeWidth="2.5" />
  680. </button>
  681. </div>
  682. {/if}
  683. <div class=" flex-1 text-2xl font-medium">
  684. <a
  685. class="hover:text-gray-500 hover:dark:text-gray-100 hover:underline flex-grow line-clamp-1"
  686. href={selectedFile.id ? `/api/v1/files/${selectedFile.id}/content` : '#'}
  687. target="_blank"
  688. >
  689. {selectedFile?.meta?.name}
  690. </a>
  691. </div>
  692. <div>
  693. <button
  694. 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"
  695. on:click={() => {
  696. updateFileContentHandler();
  697. }}
  698. >
  699. {$i18n.t('Save')}
  700. </button>
  701. </div>
  702. </div>
  703. <div
  704. class=" flex-1 w-full h-full max-h-full text-sm bg-transparent outline-none overflow-y-auto scrollbar-hidden"
  705. >
  706. {#key selectedFile.id}
  707. <RichTextInput
  708. className="input-prose-sm"
  709. bind:value={selectedFile.data.content}
  710. placeholder={$i18n.t('Add content here')}
  711. />
  712. {/key}
  713. </div>
  714. </div>
  715. {:else}
  716. <div></div>
  717. {/if}
  718. </div>
  719. </Pane>
  720. {:else if !largeScreen && selectedFileId !== null}
  721. <Drawer
  722. className="h-full"
  723. show={selectedFileId !== null}
  724. on:close={() => {
  725. selectedFileId = null;
  726. }}
  727. >
  728. <div class="flex flex-col justify-start h-full max-h-full p-2">
  729. <div class=" flex flex-col w-full h-full max-h-full">
  730. <div class="flex-shrink-0 mt-1 mb-2 flex items-center">
  731. <div class="mr-2">
  732. <button
  733. 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"
  734. on:click={() => {
  735. selectedFileId = null;
  736. }}
  737. >
  738. <ChevronLeft strokeWidth="2.5" />
  739. </button>
  740. </div>
  741. <div class=" flex-1 text-xl line-clamp-1">
  742. {selectedFile?.meta?.name}
  743. </div>
  744. <div>
  745. <button
  746. 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"
  747. on:click={() => {
  748. updateFileContentHandler();
  749. }}
  750. >
  751. {$i18n.t('Save')}
  752. </button>
  753. </div>
  754. </div>
  755. <div
  756. 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"
  757. >
  758. {#key selectedFile.id}
  759. <RichTextInput
  760. className="input-prose-sm"
  761. bind:value={selectedFile.data.content}
  762. placeholder={$i18n.t('Add content here')}
  763. />
  764. {/key}
  765. </div>
  766. </div>
  767. </div>
  768. </Drawer>
  769. {/if}
  770. </PaneGroup>
  771. </div>
  772. {:else}
  773. <Spinner />
  774. {/if}
  775. </div>