|
@@ -2,6 +2,7 @@
|
|
|
import Fuse from 'fuse.js';
|
|
|
import { toast } from 'svelte-sonner';
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
+ import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
|
|
|
|
|
import { onMount, getContext, onDestroy, tick } from 'svelte';
|
|
|
const i18n = getContext('i18n');
|
|
@@ -34,8 +35,17 @@
|
|
|
|
|
|
import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
|
|
|
import RichTextInput from '$lib/components/common/RichTextInput.svelte';
|
|
|
+ import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
|
|
+ import Drawer from '$lib/components/common/Drawer.svelte';
|
|
|
+ import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
|
|
+ import MenuLines from '$lib/components/icons/MenuLines.svelte';
|
|
|
+
|
|
|
let largeScreen = true;
|
|
|
|
|
|
+ let pane;
|
|
|
+ let showSidepanel = true;
|
|
|
+ let minSize = 0;
|
|
|
+
|
|
|
type Knowledge = {
|
|
|
id: string;
|
|
|
name: string;
|
|
@@ -458,6 +468,30 @@
|
|
|
mediaQuery.addEventListener('change', handleMediaQuery);
|
|
|
handleMediaQuery(mediaQuery);
|
|
|
|
|
|
+ // Select the container element you want to observe
|
|
|
+ const container = document.getElementById('collection-container');
|
|
|
+
|
|
|
+ // initialize the minSize based on the container width
|
|
|
+ minSize = !largeScreen ? 100 : Math.floor((300 / container.clientWidth) * 100);
|
|
|
+
|
|
|
+ // Create a new ResizeObserver instance
|
|
|
+ const resizeObserver = new ResizeObserver((entries) => {
|
|
|
+ for (let entry of entries) {
|
|
|
+ const width = entry.contentRect.width;
|
|
|
+ // calculate the percentage of 300
|
|
|
+ const percentage = (300 / width) * 100;
|
|
|
+ // set the minSize to the percentage, must be an integer
|
|
|
+ minSize = !largeScreen ? 100 : Math.floor(percentage);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Start observing the container's size changes
|
|
|
+ resizeObserver.observe(container);
|
|
|
+
|
|
|
+ if (pane) {
|
|
|
+ pane.expand();
|
|
|
+ }
|
|
|
+
|
|
|
id = $page.params.id;
|
|
|
|
|
|
const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
|
|
@@ -551,157 +585,248 @@
|
|
|
}}
|
|
|
/>
|
|
|
|
|
|
-<div class="flex flex-col w-full h-full max-h-[100dvh]">
|
|
|
+<div class="flex flex-col w-full h-full max-h-[100dvh]" id="collection-container">
|
|
|
{#if id && knowledge}
|
|
|
<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
|
|
|
- <div
|
|
|
- class=" {largeScreen
|
|
|
- ? 'flex-shrink-0'
|
|
|
- : 'flex-1'} flex py-2.5 w-80 rounded-2xl border border-gray-50 dark:border-gray-850"
|
|
|
- >
|
|
|
- <div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
|
|
|
- <div class="w-full h-full flex flex-col">
|
|
|
- <div class=" px-3">
|
|
|
- <div class="flex">
|
|
|
- <div class=" self-center ml-1 mr-3">
|
|
|
- <svg
|
|
|
- xmlns="http://www.w3.org/2000/svg"
|
|
|
- viewBox="0 0 20 20"
|
|
|
- fill="currentColor"
|
|
|
- class="w-4 h-4"
|
|
|
- >
|
|
|
- <path
|
|
|
- fill-rule="evenodd"
|
|
|
- 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"
|
|
|
- clip-rule="evenodd"
|
|
|
+ <PaneGroup direction="horizontal">
|
|
|
+ <Pane
|
|
|
+ bind:pane
|
|
|
+ defaultSize={minSize}
|
|
|
+ collapsible={true}
|
|
|
+ maxSize={50}
|
|
|
+ {minSize}
|
|
|
+ class="h-full"
|
|
|
+ onExpand={() => {
|
|
|
+ showSidepanel = true;
|
|
|
+ }}
|
|
|
+ onCollapse={() => {
|
|
|
+ showSidepanel = false;
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="{largeScreen ? 'flex-shrink-0' : 'flex-1'}
|
|
|
+ flex
|
|
|
+ py-2
|
|
|
+ rounded-2xl
|
|
|
+ border
|
|
|
+ border-gray-50
|
|
|
+ h-full
|
|
|
+ dark:border-gray-850"
|
|
|
+ >
|
|
|
+ <div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
|
|
|
+ <div class="w-full h-full flex flex-col">
|
|
|
+ <div class=" px-3">
|
|
|
+ <div class="flex py-1">
|
|
|
+ <div class=" self-center ml-1 mr-3">
|
|
|
+ <svg
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ viewBox="0 0 20 20"
|
|
|
+ fill="currentColor"
|
|
|
+ class="w-4 h-4"
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ fill-rule="evenodd"
|
|
|
+ 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"
|
|
|
+ clip-rule="evenodd"
|
|
|
+ />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <input
|
|
|
+ class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
|
|
+ bind:value={query}
|
|
|
+ placeholder={$i18n.t('Search Collection')}
|
|
|
+ on:focus={() => {
|
|
|
+ selectedFileId = null;
|
|
|
+ }}
|
|
|
/>
|
|
|
- </svg>
|
|
|
- </div>
|
|
|
- <input
|
|
|
- class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
|
|
|
- bind:value={query}
|
|
|
- placeholder={$i18n.t('Search Collection')}
|
|
|
- on:focus={() => {
|
|
|
- selectedFileId = null;
|
|
|
- }}
|
|
|
- />
|
|
|
-
|
|
|
- <div>
|
|
|
- <AddContentMenu
|
|
|
- on:upload={(e) => {
|
|
|
- if (e.detail.type === 'directory') {
|
|
|
- uploadDirectoryHandler();
|
|
|
- } else if (e.detail.type === 'text') {
|
|
|
- showAddTextContentModal = true;
|
|
|
- } else {
|
|
|
- document.getElementById('files-input').click();
|
|
|
- }
|
|
|
- }}
|
|
|
- on:sync={(e) => {
|
|
|
- showSyncConfirmModal = true;
|
|
|
- }}
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <hr class=" mt-2 mb-1 border-gray-50 dark:border-gray-850" />
|
|
|
- </div>
|
|
|
-
|
|
|
- {#if filteredItems.length > 0}
|
|
|
- <div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
|
|
|
- <Files
|
|
|
- files={filteredItems}
|
|
|
- {selectedFileId}
|
|
|
- on:click={(e) => {
|
|
|
- selectedFileId = selectedFileId === e.detail ? null : e.detail;
|
|
|
- }}
|
|
|
- on:delete={(e) => {
|
|
|
- console.log(e.detail);
|
|
|
-
|
|
|
- selectedFileId = null;
|
|
|
- deleteFileHandler(e.detail);
|
|
|
- }}
|
|
|
- />
|
|
|
- </div>
|
|
|
- {:else}
|
|
|
- <div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
|
|
|
- {/if}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
|
|
|
- {#if largeScreen}
|
|
|
- <div class="flex-1 flex justify-start h-full max-h-full pl-3">
|
|
|
- {#if selectedFile}
|
|
|
- <div class=" flex flex-col w-full h-full max-h-full">
|
|
|
- <div class="flex-shrink-0 mb-2 flex items-center">
|
|
|
- <div class=" flex-1 text-xl line-clamp-1">
|
|
|
- {selectedFile?.meta?.name}
|
|
|
+ <div>
|
|
|
+ <AddContentMenu
|
|
|
+ on:upload={(e) => {
|
|
|
+ if (e.detail.type === 'directory') {
|
|
|
+ uploadDirectoryHandler();
|
|
|
+ } else if (e.detail.type === 'text') {
|
|
|
+ showAddTextContentModal = true;
|
|
|
+ } else {
|
|
|
+ document.getElementById('files-input').click();
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ on:sync={(e) => {
|
|
|
+ showSyncConfirmModal = true;
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- <div>
|
|
|
- <button
|
|
|
- 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"
|
|
|
- on:click={() => {
|
|
|
- updateFileContentHandler();
|
|
|
- }}
|
|
|
- >
|
|
|
- {$i18n.t('Save')}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
+ {#if filteredItems.length > 0}
|
|
|
+ <div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
|
|
|
+ <Files
|
|
|
+ files={filteredItems}
|
|
|
+ {selectedFileId}
|
|
|
+ on:click={(e) => {
|
|
|
+ selectedFileId = selectedFileId === e.detail ? null : e.detail;
|
|
|
+ }}
|
|
|
+ on:delete={(e) => {
|
|
|
+ console.log(e.detail);
|
|
|
+
|
|
|
+ selectedFileId = null;
|
|
|
+ deleteFileHandler(e.detail);
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {:else}
|
|
|
+ <div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
|
|
|
+ {/if}
|
|
|
</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Pane>
|
|
|
|
|
|
- <div
|
|
|
- class=" flex-1 w-full h-full max-h-full py-2.5 px-3.5 rounded-xl text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none overflow-y-auto scrollbar-hidden"
|
|
|
- >
|
|
|
- {#key selectedFile.id}
|
|
|
- <RichTextInput
|
|
|
- className="input-prose-sm"
|
|
|
- bind:value={selectedFile.data.content}
|
|
|
- placeholder={$i18n.t('Add content here')}
|
|
|
- />
|
|
|
- {/key}
|
|
|
- </div>
|
|
|
+ {#if largeScreen}
|
|
|
+ <PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
|
|
|
+ <div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
|
|
|
+ <EllipsisVertical className="size-4 invisible group-hover:visible" />
|
|
|
</div>
|
|
|
- {:else}
|
|
|
- <div class="m-auto pb-32">
|
|
|
- <div>
|
|
|
- <div class=" flex w-full mt-1 mb-3.5">
|
|
|
- <div class="flex-1">
|
|
|
- <div class="flex items-center justify-between w-full px-0.5 mb-1">
|
|
|
- <div class="w-full">
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
|
|
|
- bind:value={knowledge.name}
|
|
|
- on:input={() => {
|
|
|
- changeDebounceHandler();
|
|
|
+ </PaneResizer>
|
|
|
+ <Pane>
|
|
|
+ <div class="flex-1 flex justify-start h-full max-h-full">
|
|
|
+ {#if selectedFile}
|
|
|
+ <div class=" flex flex-col w-full h-full max-h-full ml-2">
|
|
|
+ <div class="flex-shrink-0 mb-2 flex items-center">
|
|
|
+ {#if !showSidepanel}
|
|
|
+ <div class="-translate-x-2">
|
|
|
+ <button
|
|
|
+ class="w-full text-left text-sm p-1.5 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
|
|
+ on:click={() => {
|
|
|
+ pane.expand();
|
|
|
}}
|
|
|
- />
|
|
|
+ >
|
|
|
+ <ChevronLeft strokeWidth="2.5" />
|
|
|
+ </button>
|
|
|
</div>
|
|
|
+ {/if}
|
|
|
+
|
|
|
+ <div class=" flex-1 text-2xl font-medium line-clamp-1">
|
|
|
+ {selectedFile?.meta?.name}
|
|
|
</div>
|
|
|
|
|
|
- <div class="flex w-full px-1">
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- class="text-center w-full text-gray-500 bg-transparent outline-none"
|
|
|
- bind:value={knowledge.description}
|
|
|
- on:input={() => {
|
|
|
- changeDebounceHandler();
|
|
|
+ <div>
|
|
|
+ <button
|
|
|
+ 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"
|
|
|
+ on:click={() => {
|
|
|
+ updateFileContentHandler();
|
|
|
}}
|
|
|
+ >
|
|
|
+ {$i18n.t('Save')}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ class=" flex-1 w-full h-full max-h-full text-sm bg-transparent outline-none overflow-y-auto scrollbar-hidden"
|
|
|
+ >
|
|
|
+ {#key selectedFile.id}
|
|
|
+ <RichTextInput
|
|
|
+ className="input-prose-sm"
|
|
|
+ bind:value={selectedFile.data.content}
|
|
|
+ placeholder={$i18n.t('Add content here')}
|
|
|
/>
|
|
|
+ {/key}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {:else}
|
|
|
+ <div class="m-auto pb-32">
|
|
|
+ <div>
|
|
|
+ <div class=" flex w-full mt-1 mb-3.5">
|
|
|
+ <div class="flex-1">
|
|
|
+ <div class="flex items-center justify-between w-full px-0.5 mb-1">
|
|
|
+ <div class="w-full">
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
|
|
|
+ bind:value={knowledge.name}
|
|
|
+ on:input={() => {
|
|
|
+ changeDebounceHandler();
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex w-full px-1">
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ class="text-center w-full text-gray-500 bg-transparent outline-none"
|
|
|
+ bind:value={knowledge.description}
|
|
|
+ on:input={() => {
|
|
|
+ changeDebounceHandler();
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div class=" mt-2 text-center text-sm text-gray-200 dark:text-gray-700 w-full">
|
|
|
+ {$i18n.t('Select a file to view or drag and drop a file to upload')}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {/if}
|
|
|
+ </div>
|
|
|
+ </Pane>
|
|
|
+ {:else if !largeScreen && selectedFileId !== null}
|
|
|
+ <Drawer
|
|
|
+ className="h-full"
|
|
|
+ show={selectedFileId !== null}
|
|
|
+ on:close={() => {
|
|
|
+ selectedFileId = null;
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div class="flex flex-col justify-start h-full max-h-full p-2">
|
|
|
+ <div class=" flex flex-col w-full h-full max-h-full">
|
|
|
+ <div class="flex-shrink-0 mb-2 flex items-center">
|
|
|
+ <div class="mr-2">
|
|
|
+ <button
|
|
|
+ class="w-full text-left text-sm p-1.5 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
|
|
|
+ on:click={() => {
|
|
|
+ selectedFileId = null;
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <ChevronLeft strokeWidth="2.5" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class=" flex-1 text-xl line-clamp-1">
|
|
|
+ {selectedFile?.meta?.name}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <button
|
|
|
+ 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"
|
|
|
+ on:click={() => {
|
|
|
+ updateFileContentHandler();
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {$i18n.t('Save')}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- <div class=" mt-2 text-center text-sm text-gray-200 dark:text-gray-700 w-full">
|
|
|
- {$i18n.t('Select a file to view or drag and drop a file to upload')}
|
|
|
+ <div
|
|
|
+ class=" flex-1 w-full h-full max-h-full py-2.5 px-3.5 rounded-xl text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none overflow-y-auto scrollbar-hidden"
|
|
|
+ >
|
|
|
+ {#key selectedFile.id}
|
|
|
+ <RichTextInput
|
|
|
+ className="input-prose-sm"
|
|
|
+ bind:value={selectedFile.data.content}
|
|
|
+ placeholder={$i18n.t('Add content here')}
|
|
|
+ />
|
|
|
+ {/key}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- {/if}
|
|
|
- </div>
|
|
|
- {/if}
|
|
|
+ </Drawer>
|
|
|
+ {/if}
|
|
|
+ </PaneGroup>
|
|
|
</div>
|
|
|
{:else}
|
|
|
<Spinner />
|