Collection.svelte 23 KB

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