Timothy Jaeryang Baek 5 meses atrás
pai
commit
aed2caefe1

+ 55 - 40
src/lib/components/common/FileItem.svelte

@@ -5,6 +5,7 @@
 	import FileItemModal from './FileItemModal.svelte';
 	import GarbageBin from '../icons/GarbageBin.svelte';
 	import Spinner from './Spinner.svelte';
+	import Tooltip from './Tooltip.svelte';
 
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
@@ -18,6 +19,7 @@
 
 	export let item = null;
 	export let edit = false;
+	export let small = false;
 
 	export let name: string;
 	export let type: string;
@@ -31,7 +33,7 @@
 {/if}
 
 <button
-	class="relative group p-1.5 {className} flex items-center {colorClassName} rounded-2xl text-left"
+	class="relative group p-1.5 {className} flex items-center gap-1 {colorClassName} rounded-xl text-left"
 	type="button"
 	on:click={async () => {
 		if (item?.file?.data?.content) {
@@ -49,48 +51,61 @@
 		dispatch('click');
 	}}
 >
-	<div class="p-3 bg-black/20 dark:bg-white/10 text-white rounded-xl">
-		{#if !loading}
-			<svg
-				xmlns="http://www.w3.org/2000/svg"
-				viewBox="0 0 24 24"
-				fill="currentColor"
-				class=" size-5"
-			>
-				<path
-					fill-rule="evenodd"
-					d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
-					clip-rule="evenodd"
-				/>
-				<path
-					d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
-				/>
-			</svg>
-		{:else}
-			<Spinner />
-		{/if}
-	</div>
-
-	<div class="flex flex-col justify-center -space-y-0.5 ml-1 px-2.5 w-full">
-		<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1">
-			{name}
-		</div>
-
-		<div class=" flex justify-between text-gray-500 text-xs line-clamp-1">
-			{#if type === 'file'}
-				{$i18n.t('File')}
-			{:else if type === 'doc'}
-				{$i18n.t('Document')}
-			{:else if type === 'collection'}
-				{$i18n.t('Collection')}
+	{#if !small}
+		<div class="p-3 bg-black/20 dark:bg-white/10 text-white rounded-xl">
+			{#if !loading}
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					viewBox="0 0 24 24"
+					fill="currentColor"
+					class=" size-5"
+				>
+					<path
+						fill-rule="evenodd"
+						d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
+						clip-rule="evenodd"
+					/>
+					<path
+						d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
+					/>
+				</svg>
 			{:else}
-				<span class=" capitalize line-clamp-1">{type}</span>
-			{/if}
-			{#if size}
-				<span class="capitalize">{formatFileSize(size)}</span>
+				<Spinner />
 			{/if}
 		</div>
-	</div>
+	{/if}
+
+	{#if !small}
+		<div class="flex flex-col justify-center -space-y-0.5 px-2.5 w-full">
+			<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1">
+				{name}
+			</div>
+
+			<div class=" flex justify-between text-gray-500 text-xs line-clamp-1">
+				{#if type === 'file'}
+					{$i18n.t('File')}
+				{:else if type === 'doc'}
+					{$i18n.t('Document')}
+				{:else if type === 'collection'}
+					{$i18n.t('Collection')}
+				{:else}
+					<span class=" capitalize line-clamp-1">{type}</span>
+				{/if}
+				{#if size}
+					<span class="capitalize">{formatFileSize(size)}</span>
+				{/if}
+			</div>
+		</div>
+	{:else}
+		<Tooltip content={name} className="flex flex-col w-full" placement="top-start">
+			<div class="flex flex-col justify-center -space-y-0.5 px-2.5 w-full">
+				<div class=" dark:text-gray-100 text-sm flex justify-between items-center">
+					<div class="font-medium line-clamp-1">{name}</div>
+					<div class="text-gray-500 text-xs capitalize shrink-0">{formatFileSize(size)}</div>
+				</div>
+			</div>
+		</Tooltip>
+	{/if}
 
 	{#if dismissible}
 		<div class=" absolute -top-1 -right-1">

+ 12 - 10
src/lib/components/common/RichTextInput.svelte

@@ -187,7 +187,7 @@
 					: [])
 			],
 			content: content,
-			autofocus: true,
+			autofocus: messageInput ? true : false,
 			onTransaction: () => {
 				// force re-render so `editor.isActive` works as expected
 				editor = editor;
@@ -218,16 +218,16 @@
 						return false;
 					},
 					keydown: (view, event) => {
-						// Handle Tab Key
-						if (event.key === 'Tab') {
-							const handled = selectNextTemplate(view.state, view.dispatch);
-							if (handled) {
-								event.preventDefault();
-								return true;
+						if (messageInput) {
+							// Handle Tab Key
+							if (event.key === 'Tab') {
+								const handled = selectNextTemplate(view.state, view.dispatch);
+								if (handled) {
+									event.preventDefault();
+									return true;
+								}
 							}
-						}
 
-						if (messageInput) {
 							if (event.key === 'Enter') {
 								// Check if the current selection is inside a structured block (like codeBlock or list)
 								const { state } = view;
@@ -317,7 +317,9 @@
 			}
 		});
 
-		selectTemplate();
+		if (messageInput) {
+			selectTemplate();
+		}
 	});
 
 	onDestroy(() => {

+ 182 - 206
src/lib/components/workspace/Knowledge/KnowledgeBase.svelte

@@ -600,7 +600,7 @@
 	}}
 />
 
-<div class="flex flex-col w-full h-full max-h-[100dvh] translate-y-1" id="collection-container">
+<div class="flex flex-col w-full translate-y-1" id="collection-container">
 	{#if id && knowledge}
 		<AccessControlModal
 			bind:show={showAccessControlModal}
@@ -657,229 +657,205 @@
 			</div>
 		</div>
 
-		<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
-			<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;
+		<div class="flex flex-row flex-1 h-full max-h-full pb-2.5 gap-3">
+			{#if largeScreen}
+				<div class="flex-1 flex justify-start w-full h-full max-h-full">
+					{#if selectedFile}
+						<div class=" flex flex-col w-full h-full max-h-full">
+							<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();
 											}}
-										/>
-
-										<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>
+										>
+											<ChevronLeft strokeWidth="2.5" />
+										</button>
 									</div>
-								</div>
+								{/if}
 
-								{#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);
+								<div class=" flex-1 text-xl font-medium">
+									<a
+										class="hover:text-gray-500 hover:dark:text-gray-100 hover:underline flex-grow line-clamp-1"
+										href={selectedFile.id ? `/api/v1/files/${selectedFile.id}/content` : '#'}
+										target="_blank"
+									>
+										{selectedFile?.meta?.name}
+									</a>
+								</div>
 
-												selectedFileId = null;
-												deleteFileHandler(e.detail);
-											}}
-										/>
-									</div>
-								{:else}
-									<div
-										class="m-auto flex flex-col justify-center text-center text-gray-500 text-xs"
+								<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();
+										}}
 									>
-										<div>
-											{$i18n.t('No content found')}
-										</div>
+										{$i18n.t('Save')}
+									</button>
+								</div>
+							</div>
 
-										<div class="mx-12 mt-2 text-center text-gray-200 dark:text-gray-700">
-											{$i18n.t('Drag and drop a file to upload or select a file to view')}
-										</div>
-									</div>
-								{/if}
+							<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')}
+										preserveBreaks={true}
+									/>
+								{/key}
 							</div>
 						</div>
-					</div>
-				</Pane>
-
-				{#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" />
+					{:else}
+						<div class="h-full flex w-full">
+							<div class="m-auto text-xs text-center text-gray-200 dark:text-gray-700">
+								{$i18n.t('Drag and drop a file to upload or select a file to view')}
+							</div>
 						</div>
-					</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.5">
-									<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">
-											<a
-												class="hover:text-gray-500 hover:dark:text-gray-100 hover:underline flex-grow line-clamp-1"
-												href={selectedFile.id ? `/api/v1/files/${selectedFile.id}/content` : '#'}
-												target="_blank"
-											>
-												{selectedFile?.meta?.name}
-											</a>
-										</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
-										class=" flex-1 w-full h-full max-h-full text-sm bg-transparent outline-none overflow-y-auto scrollbar-hidden"
+					{/if}
+				</div>
+			{: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 mt-1 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;
+										}}
 									>
-										{#key selectedFile.id}
-											<RichTextInput
-												className="input-prose-sm"
-												bind:value={selectedFile.data.content}
-												placeholder={$i18n.t('Add content here')}
-												preserveBreaks={true}
-											/>
-										{/key}
-									</div>
+										<ChevronLeft strokeWidth="2.5" />
+									</button>
+								</div>
+								<div class=" flex-1 text-xl line-clamp-1">
+									{selectedFile?.meta?.name}
 								</div>
-							{:else}
-								<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 mt-1 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>
+									<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 py-2.5 px-3.5 rounded-lg text-sm bg-transparent overflow-y-auto scrollbar-hidden"
-								>
-									{#key selectedFile.id}
-										<RichTextInput
-											className="input-prose-sm"
-											bind:value={selectedFile.data.content}
-											placeholder={$i18n.t('Add content here')}
+							<div
+								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"
+							>
+								{#key selectedFile.id}
+									<RichTextInput
+										className="input-prose-sm"
+										bind:value={selectedFile.data.content}
+										placeholder={$i18n.t('Add content here')}
+										preserveBreaks={true}
+									/>
+								{/key}
+							</div>
+						</div>
+					</div>
+				</Drawer>
+			{/if}
+
+			<div
+				class="{largeScreen ? 'flex-shrink-0 w-72 max-w-72' : '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 mb-0.5">
+								<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"
 										/>
-									{/key}
+									</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>
 						</div>
-					</Drawer>
-				{/if}
-			</PaneGroup>
+
+						{#if filteredItems.length > 0}
+							<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
+								<Files
+									small
+									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="my-3 flex flex-col justify-center text-center text-gray-500 text-xs">
+								<div>
+									{$i18n.t('No content found')}
+								</div>
+							</div>
+						{/if}
+					</div>
+				</div>
+			</div>
 		</div>
 	{:else}
 		<Spinner />

+ 3 - 0
src/lib/components/workspace/Knowledge/KnowledgeBase/Files.svelte

@@ -6,6 +6,8 @@
 
 	export let selectedFileId = null;
 	export let files = [];
+
+	export let small = false;
 </script>
 
 <div class=" max-h-full flex flex-col w-full">
@@ -16,6 +18,7 @@
 				colorClassName="{selectedFileId === file.id
 					? ' bg-gray-50 dark:bg-gray-850'
 					: 'bg-transparent'} hover:bg-gray-50 dark:hover:bg-gray-850 transition"
+				{small}
 				{file}
 				name={file?.name ?? file?.meta?.name}
 				type="file"