ソースを参照

enh: enter into folder

Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
Timothy Jaeryang Baek 2 ヶ月 前
コミット
5abc03f4dd

+ 2 - 1
src/lib/components/chat/Chat.svelte

@@ -36,7 +36,8 @@
 		chatTitle,
 		showArtifacts,
 		tools,
-		toolServers
+		toolServers,
+		selectedFolder
 	} from '$lib/stores';
 	import {
 		convertMessagesToHistory,

+ 120 - 85
src/lib/components/chat/Placeholder.svelte

@@ -7,7 +7,13 @@
 
 	const dispatch = createEventDispatcher();
 
-	import { config, user, models as _models, temporaryChatEnabled } from '$lib/stores';
+	import {
+		config,
+		user,
+		models as _models,
+		temporaryChatEnabled,
+		selectedFolder
+	} from '$lib/stores';
 	import { sanitizeResponseContent, extractCurlyBraceWords } from '$lib/utils';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
@@ -15,6 +21,9 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
 	import MessageInput from './MessageInput.svelte';
+	import FolderOpen from '../icons/FolderOpen.svelte';
+	import XMark from '../icons/XMark.svelte';
+	import Folder from '../icons/Folder.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -77,103 +86,129 @@
 		class="w-full text-3xl text-gray-800 dark:text-gray-100 text-center flex items-center gap-4 font-primary"
 	>
 		<div class="w-full flex flex-col justify-center items-center">
-			<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5 max-w-xl">
-				<div class="flex shrink-0 justify-center">
-					<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
-						{#each models as model, modelIdx}
+			{#if $selectedFolder}
+				<div class="mb-3 px-4 justify-center w-fit flex relative group">
+					<div class="text-center flex gap-3.5 items-center">
+						<div class=" rounded-full bg-gray-50 dark:bg-gray-800 p-3 w-fit">
+							<Folder className="size-4.5" strokeWidth="2" />
+						</div>
+
+						<div class="text-3xl">
+							{$selectedFolder?.name}
+						</div>
+					</div>
+
+					<div class="absolute -right-3">
+						<button
+							class="group-hover:visible invisible rounded-md"
+							type="button"
+							on:click={() => {
+								selectedFolder.set(null);
+							}}
+						>
+							<XMark className="size-4" />
+						</button>
+					</div>
+				</div>
+			{:else}
+				<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5 max-w-xl">
+					<div class="flex shrink-0 justify-center">
+						<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
+							{#each models as model, modelIdx}
+								<Tooltip
+									content={(models[modelIdx]?.info?.meta?.tags ?? [])
+										.map((tag) => tag.name.toUpperCase())
+										.join(', ')}
+									placement="top"
+								>
+									<button
+										aria-hidden={models.length <= 1}
+										aria-label={$i18n.t('Get information on {{name}} in the UI', {
+											name: models[modelIdx]?.name
+										})}
+										on:click={() => {
+											selectedModelIdx = modelIdx;
+										}}
+									>
+										<img
+											crossorigin="anonymous"
+											src={model?.info?.meta?.profile_image_url ??
+												($i18n.language === 'dg-DG'
+													? `${WEBUI_BASE_URL}/doge.png`
+													: `${WEBUI_BASE_URL}/static/favicon.png`)}
+											class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
+											aria-hidden="true"
+											draggable="false"
+										/>
+									</button>
+								</Tooltip>
+							{/each}
+						</div>
+					</div>
+
+					<div
+						class=" text-3xl @sm:text-3xl line-clamp-1 flex items-center"
+						in:fade={{ duration: 100 }}
+					>
+						{#if models[selectedModelIdx]?.name}
 							<Tooltip
-								content={(models[modelIdx]?.info?.meta?.tags ?? [])
-									.map((tag) => tag.name.toUpperCase())
-									.join(', ')}
+								content={models[selectedModelIdx]?.name}
 								placement="top"
+								className=" flex items-center "
 							>
-								<button
-									aria-hidden={models.length <= 1}
-									aria-label={$i18n.t('Get information on {{name}} in the UI', {
-										name: models[modelIdx]?.name
-									})}
-									on:click={() => {
-										selectedModelIdx = modelIdx;
-									}}
-								>
-									<img
-										crossorigin="anonymous"
-										src={model?.info?.meta?.profile_image_url ??
-											($i18n.language === 'dg-DG'
-												? `${WEBUI_BASE_URL}/doge.png`
-												: `${WEBUI_BASE_URL}/static/favicon.png`)}
-										class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
-										aria-hidden="true"
-										draggable="false"
-									/>
-								</button>
+								<span class="line-clamp-1">
+									{models[selectedModelIdx]?.name}
+								</span>
 							</Tooltip>
-						{/each}
+						{:else}
+							{$i18n.t('Hello, {{name}}', { name: $user?.name })}
+						{/if}
 					</div>
 				</div>
 
-				<div
-					class=" text-3xl @sm:text-3xl line-clamp-1 flex items-center"
-					in:fade={{ duration: 100 }}
-				>
-					{#if models[selectedModelIdx]?.name}
-						<Tooltip
-							content={models[selectedModelIdx]?.name}
-							placement="top"
-							className=" flex items-center "
-						>
-							<span class="line-clamp-1">
-								{models[selectedModelIdx]?.name}
-							</span>
-						</Tooltip>
-					{:else}
-						{$i18n.t('Hello, {{name}}', { name: $user?.name })}
-					{/if}
-				</div>
-			</div>
-
-			<div class="flex mt-1 mb-2">
-				<div in:fade={{ duration: 100, delay: 50 }}>
-					{#if models[selectedModelIdx]?.info?.meta?.description ?? null}
-						<Tooltip
-							className=" w-fit"
-							content={marked.parse(
-								sanitizeResponseContent(
-									models[selectedModelIdx]?.info?.meta?.description ?? ''
-								).replaceAll('\n', '<br>')
-							)}
-							placement="top"
-						>
-							<div
-								class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
-							>
-								{@html marked.parse(
+				<div class="flex mt-1 mb-2">
+					<div in:fade={{ duration: 100, delay: 50 }}>
+						{#if models[selectedModelIdx]?.info?.meta?.description ?? null}
+							<Tooltip
+								className=" w-fit"
+								content={marked.parse(
 									sanitizeResponseContent(
 										models[selectedModelIdx]?.info?.meta?.description ?? ''
 									).replaceAll('\n', '<br>')
 								)}
-							</div>
-						</Tooltip>
-
-						{#if models[selectedModelIdx]?.info?.meta?.user}
-							<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
-								By
-								{#if models[selectedModelIdx]?.info?.meta?.user.community}
-									<a
-										href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user
-											.username}"
-										>{models[selectedModelIdx]?.info?.meta?.user.name
-											? models[selectedModelIdx]?.info?.meta?.user.name
-											: `@${models[selectedModelIdx]?.info?.meta?.user.username}`}</a
-									>
-								{:else}
-									{models[selectedModelIdx]?.info?.meta?.user.name}
-								{/if}
-							</div>
+								placement="top"
+							>
+								<div
+									class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
+								>
+									{@html marked.parse(
+										sanitizeResponseContent(
+											models[selectedModelIdx]?.info?.meta?.description ?? ''
+										).replaceAll('\n', '<br>')
+									)}
+								</div>
+							</Tooltip>
+
+							{#if models[selectedModelIdx]?.info?.meta?.user}
+								<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
+									By
+									{#if models[selectedModelIdx]?.info?.meta?.user.community}
+										<a
+											href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user
+												.username}"
+											>{models[selectedModelIdx]?.info?.meta?.user.name
+												? models[selectedModelIdx]?.info?.meta?.user.name
+												: `@${models[selectedModelIdx]?.info?.meta?.user.username}`}</a
+										>
+									{:else}
+										{models[selectedModelIdx]?.info?.meta?.user.name}
+									{/if}
+								</div>
+							{/if}
 						{/if}
-					{/if}
+					</div>
 				</div>
-			</div>
+			{/if}
 
 			<div class="text-base font-normal @md:max-w-3xl w-full py-3 {atSelectedModel ? 'mt-2' : ''}">
 				<MessageInput

+ 19 - 0
src/lib/components/icons/Folder.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z"
+	/>
+</svg>

+ 3 - 1
src/lib/components/layout/Sidebar.svelte

@@ -22,7 +22,8 @@
 		socket,
 		config,
 		isApp,
-		models
+		models,
+		selectedFolder
 	} from '$lib/stores';
 	import { onMount, getContext, tick, onDestroy } from 'svelte';
 
@@ -494,6 +495,7 @@
 				draggable="false"
 				on:click={async () => {
 					selectedChatId = null;
+					selectedFolder.set(null);
 
 					if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
 						await temporaryChatEnabled.set(true);

+ 8 - 4
src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte

@@ -12,6 +12,10 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Download from '$lib/components/icons/Download.svelte';
 
+	export let onEdit = () => {};
+	export let onExport = () => {};
+	export let onDelete = () => {};
+
 	let show = false;
 </script>
 
@@ -38,17 +42,17 @@
 			<DropdownMenu.Item
 				class="flex gap-2 items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
-					dispatch('rename');
+					onEdit();
 				}}
 			>
 				<Pencil strokeWidth="2" />
-				<div class="flex items-center">{$i18n.t('Rename')}</div>
+				<div class="flex items-center">{$i18n.t('Edit')}</div>
 			</DropdownMenu.Item>
 
 			<DropdownMenu.Item
 				class="flex gap-2 items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
-					dispatch('export');
+					onExport();
 				}}
 			>
 				<Download strokeWidth="2" />
@@ -59,7 +63,7 @@
 			<DropdownMenu.Item
 				class="flex  gap-2  items-center px-3 py-1.5 text-sm  cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				on:click={() => {
-					dispatch('delete');
+					onDelete();
 				}}
 			>
 				<GarbageBin strokeWidth="2" />

+ 19 - 4
src/lib/components/layout/Sidebar/RecursiveFolder.svelte

@@ -31,6 +31,7 @@
 	import ChatItem from './ChatItem.svelte';
 	import FolderMenu from './Folders/FolderMenu.svelte';
 	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+	import { selectedFolder } from '$lib/stores';
 
 	export let open = false;
 
@@ -38,6 +39,8 @@
 	export let folderId;
 	export let shiftKey = false;
 
+	export let onCreateChat = (e) => {};
+
 	export let className = '';
 
 	export let parentDragged = false;
@@ -288,6 +291,11 @@
 		if (res) {
 			folders[folderId].name = name;
 			toast.success($i18n.t('Folder name updated successfully'));
+
+			if ($selectedFolder?.id === folderId) {
+				selectedFolder.set(folders[folderId]);
+			}
+
 			dispatch('update');
 		}
 	};
@@ -394,10 +402,16 @@
 		<div class="w-full group">
 			<button
 				id="folder-{folderId}-button"
-				class="relative w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
+				class="relative w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition {$selectedFolder?.id ===
+				folderId
+					? 'bg-gray-100 dark:bg-gray-900'
+					: ''}"
 				on:dblclick={() => {
 					editHandler();
 				}}
+				on:click={(e) => {
+					selectedFolder.set(folders[folderId]);
+				}}
 			>
 				<div class="text-gray-300 dark:text-gray-600">
 					{#if open}
@@ -446,18 +460,19 @@
 					on:pointerup={(e) => {
 						e.stopPropagation();
 					}}
+					on:click={(e) => e.stopPropagation()}
 				>
 					<FolderMenu
-						on:rename={() => {
+						onEdit={() => {
 							// Requires a timeout to prevent the click event from closing the dropdown
 							setTimeout(() => {
 								editHandler();
 							}, 200);
 						}}
-						on:delete={() => {
+						onDelete={() => {
 							showDeleteConfirm = true;
 						}}
-						on:export={() => {
+						onExport={() => {
 							exportHandler();
 						}}
 					>