Ver código fonte

enh: width adjustable chat controls

Timothy J. Baek 7 meses atrás
pai
commit
692f04d457

+ 12 - 0
package-lock.json

@@ -32,6 +32,7 @@
 				"katex": "^0.16.9",
 				"marked": "^9.1.0",
 				"mermaid": "^10.9.1",
+				"paneforge": "^0.0.6",
 				"pyodide": "^0.26.1",
 				"socket.io-client": "^4.2.0",
 				"sortablejs": "^1.15.2",
@@ -6986,6 +6987,17 @@
 				"url": "https://github.com/sponsors/sindresorhus"
 			}
 		},
+		"node_modules/paneforge": {
+			"version": "0.0.6",
+			"resolved": "https://registry.npmjs.org/paneforge/-/paneforge-0.0.6.tgz",
+			"integrity": "sha512-jYeN/wdREihja5c6nK3S5jritDQ+EbCqC5NrDo97qCZzZ9GkmEcN5C0ZCjF4nmhBwkDKr6tLIgz4QUKWxLXjAw==",
+			"dependencies": {
+				"nanoid": "^5.0.4"
+			},
+			"peerDependencies": {
+				"svelte": "^4.0.0 || ^5.0.0-next.1"
+			}
+		},
 		"node_modules/parent-module": {
 			"version": "1.0.1",
 			"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",

+ 1 - 0
package.json

@@ -72,6 +72,7 @@
 		"katex": "^0.16.9",
 		"marked": "^9.1.0",
 		"mermaid": "^10.9.1",
+		"paneforge": "^0.0.6",
 		"pyodide": "^0.26.1",
 		"socket.io-client": "^4.2.0",
 		"sortablejs": "^1.15.2",

+ 109 - 105
src/lib/components/chat/Chat.svelte

@@ -2,6 +2,7 @@
 	import { v4 as uuidv4 } from 'uuid';
 	import { toast } from 'svelte-sonner';
 	import mermaid from 'mermaid';
+	import { PaneGroup, Pane, PaneResizer } from 'paneforge';
 
 	import { getContext, onDestroy, onMount, tick } from 'svelte';
 	import { goto } from '$app/navigation';
@@ -26,7 +27,8 @@
 		showControls,
 		showCallOverlay,
 		currentChatPage,
-		temporaryChatEnabled
+		temporaryChatEnabled,
+		mobile
 	} from '$lib/stores';
 	import {
 		convertMessagesToHistory,
@@ -64,12 +66,14 @@
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import ChatControls from './ChatControls.svelte';
 	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
+	import EllipsisVertical from '../icons/EllipsisVertical.svelte';
 
 	const i18n: Writable<i18nType> = getContext('i18n');
 
 	export let chatIdProp = '';
 	let loaded = false;
 	const eventTarget = new EventTarget();
+	let controlPane;
 
 	let stopResponseFlag = false;
 	let autoScroll = true;
@@ -1760,117 +1764,117 @@
 			bind:selectedModels
 			bind:showModelSelector
 			shareEnabled={messages.length > 0}
+			{controlPane}
 			{chat}
 			{initNewChat}
 		/>
 
-		{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
-			<div
-				class="absolute top-[4.25rem] w-full {$showSidebar
-					? 'md:max-w-[calc(100%-260px)]'
-					: ''} {$showControls ? 'lg:pr-[26rem]' : ''} z-20"
-			>
-				<div class=" flex flex-col gap-1 w-full">
-					{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
-						<Banner
-							{banner}
-							on:dismiss={(e) => {
-								const bannerId = e.detail;
-
-								localStorage.setItem(
-									'dismissedBannerIds',
-									JSON.stringify(
-										[
-											bannerId,
-											...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
-										].filter((id) => $banners.find((b) => b.id === id))
-									)
-								);
+		<PaneGroup direction="horizontal" class="w-full h-full">
+			<Pane defaultSize={50} class="h-full flex w-full relative">
+				{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
+					<div class="absolute top-3 left-0 right-0 w-full z-20">
+						<div class=" flex flex-col gap-1 w-full">
+							{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
+								<Banner
+									{banner}
+									on:dismiss={(e) => {
+										const bannerId = e.detail;
+
+										localStorage.setItem(
+											'dismissedBannerIds',
+											JSON.stringify(
+												[
+													bannerId,
+													...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
+												].filter((id) => $banners.find((b) => b.id === id))
+											)
+										);
+									}}
+								/>
+							{/each}
+						</div>
+					</div>
+				{/if}
+
+				<div class="flex flex-col flex-auto z-10 w-full">
+					<div
+						class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden"
+						id="messages-container"
+						bind:this={messagesContainerElement}
+						on:scroll={(e) => {
+							autoScroll =
+								messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
+								messagesContainerElement.clientHeight + 5;
+						}}
+					>
+						<div class=" h-full w-full flex flex-col {chatIdProp ? 'py-4' : 'pt-2 pb-4'}">
+							<Messages
+								chatId={$chatId}
+								{selectedModels}
+								{processing}
+								bind:history
+								bind:messages
+								bind:autoScroll
+								bind:prompt
+								bottomPadding={files.length > 0}
+								{sendPrompt}
+								{continueGeneration}
+								{regenerateResponse}
+								{mergeResponses}
+								{chatActionHandler}
+								{showMessage}
+							/>
+						</div>
+					</div>
+
+					<div class="">
+						<MessageInput
+							bind:files
+							bind:prompt
+							bind:autoScroll
+							bind:selectedToolIds
+							bind:webSearchEnabled
+							bind:atSelectedModel
+							availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
+								const model = $models.find((m) => m.id === e);
+								if (model?.info?.meta?.toolIds ?? false) {
+									return [...new Set([...a, ...model.info.meta.toolIds])];
+								}
+								return a;
+							}, [])}
+							transparentBackground={$settings?.backgroundImageUrl ?? false}
+							{selectedModels}
+							{messages}
+							{submitPrompt}
+							{stopResponse}
+							on:call={() => {
+								showControls.set(true);
 							}}
 						/>
-					{/each}
+					</div>
 				</div>
-			</div>
-		{/if}
+			</Pane>
 
-		<div class="flex flex-col flex-auto z-10">
-			<div
-				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden {$showControls
-					? 'lg:pr-[26rem]'
-					: ''}"
-				id="messages-container"
-				bind:this={messagesContainerElement}
-				on:scroll={(e) => {
-					autoScroll =
-						messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
-						messagesContainerElement.clientHeight + 5;
-				}}
-			>
-				<div class=" h-full w-full flex flex-col {chatIdProp ? 'py-4' : 'pt-2 pb-4'}">
-					<Messages
-						chatId={$chatId}
-						{selectedModels}
-						{processing}
-						bind:history
-						bind:messages
-						bind:autoScroll
-						bind:prompt
-						bottomPadding={files.length > 0}
-						{sendPrompt}
-						{continueGeneration}
-						{regenerateResponse}
-						{mergeResponses}
-						{chatActionHandler}
-						{showMessage}
-					/>
-				</div>
-			</div>
-
-			<div class={$showControls ? 'lg:pr-[26rem]' : ''}>
-				<MessageInput
-					bind:files
-					bind:prompt
-					bind:autoScroll
-					bind:selectedToolIds
-					bind:webSearchEnabled
-					bind:atSelectedModel
-					availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
-						const model = $models.find((m) => m.id === e);
-						if (model?.info?.meta?.toolIds ?? false) {
-							return [...new Set([...a, ...model.info.meta.toolIds])];
-						}
-						return a;
-					}, [])}
-					transparentBackground={$settings?.backgroundImageUrl ?? false}
-					{selectedModels}
-					{messages}
-					{submitPrompt}
-					{stopResponse}
-					on:call={() => {
-						showControls.set(true);
-					}}
-				/>
-			</div>
-		</div>
+			<ChatControls
+				models={selectedModelIds.reduce((a, e, i, arr) => {
+					const model = $models.find((m) => m.id === e);
+					if (model) {
+						return [...a, model];
+					}
+					return a;
+				}, [])}
+				bind:history
+				bind:chatFiles
+				bind:params
+				bind:files
+				bind:pane={controlPane}
+				{submitPrompt}
+				{stopResponse}
+				{showMessage}
+				modelId={selectedModelIds?.at(0) ?? null}
+				chatId={$chatId}
+				{eventTarget}
+			/>
+		</PaneGroup>
 	</div>
 {/if}
-
-<ChatControls
-	models={selectedModelIds.reduce((a, e, i, arr) => {
-		const model = $models.find((m) => m.id === e);
-		if (model) {
-			return [...a, model];
-		}
-		return a;
-	}, [])}
-	bind:history
-	bind:chatFiles
-	bind:params
-	bind:files
-	{submitPrompt}
-	{stopResponse}
-	{showMessage}
-	modelId={selectedModelIds?.at(0) ?? null}
-	chatId={$chatId}
-	{eventTarget}
-/>

+ 50 - 26
src/lib/components/chat/ChatControls.svelte

@@ -10,6 +10,9 @@
 	import CallOverlay from './MessageInput/CallOverlay.svelte';
 	import Drawer from '../common/Drawer.svelte';
 	import Overview from './Overview.svelte';
+	import { Pane, PaneResizer } from 'paneforge';
+	import EllipsisVertical from '../icons/EllipsisVertical.svelte';
+	import { get } from 'svelte/store';
 
 	export let history;
 	export let models = [];
@@ -25,7 +28,9 @@
 	export let files;
 	export let modelId;
 
+	export let pane;
 	let largeScreen = false;
+
 	onMount(() => {
 		// listen to resize 1024px
 		const mediaQuery = window.matchMedia('(min-width: 1024px)');
@@ -58,33 +63,33 @@
 
 <SvelteFlowProvider>
 	{#if !largeScreen}
-		{#if $showCallOverlay}
-			<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
-				<div
-					class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
-				>
-					<CallOverlay
-						bind:files
-						{submitPrompt}
-						{stopResponse}
-						{modelId}
-						{chatId}
-						{eventTarget}
-						on:close={() => {
-							showControls.set(false);
-						}}
-					/>
-				</div>
-			</div>
-		{:else if $showControls}
+		{#if $showControls}
 			<Drawer
 				show={$showControls}
 				on:close={() => {
 					showControls.set(false);
 				}}
 			>
-				<div class=" {$showOverview ? ' h-screen  w-screen' : 'px-6 py-4'} h-full">
-					{#if $showOverview}
+				<div
+					class=" {$showCallOverlay || $showOverview ? ' h-screen  w-screen' : 'px-6 py-4'} h-full"
+				>
+					{#if $showCallOverlay}
+						<div
+							class=" h-full max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
+						>
+							<CallOverlay
+								bind:files
+								{submitPrompt}
+								{stopResponse}
+								{modelId}
+								{chatId}
+								{eventTarget}
+								on:close={() => {
+									showControls.set(false);
+								}}
+							/>
+						</div>
+					{:else if $showOverview}
 						<Overview
 							{history}
 							on:nodeclick={(e) => {
@@ -107,11 +112,30 @@
 				</div>
 			</Drawer>
 		{/if}
-	{:else if $showControls}
-		<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none">
-			<div class="pr-4 pt-14 pb-8 w-[26rem] h-full" in:slide={{ duration: 200, axis: 'x' }}>
+	{:else}
+		<!-- if $showControls -->
+		<PaneResizer class="relative flex w-2 items-center justify-center bg-background">
+			<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
+				<EllipsisVertical />
+			</div>
+		</PaneResizer>
+		<Pane
+			bind:pane
+			defaultSize={$showControls ? localStorage.getItem('chat-controls-size') || 40 : 0}
+			onResize={(size) => {
+				if (size === 0) {
+					showControls.set(false);
+				} else {
+					if (!$showControls) {
+						showControls.set(true);
+					}
+					localStorage.setItem('chat-controls-size', size);
+				}
+			}}
+		>
+			<div class="pr-4 pb-8 flex max-h-full min-h-full" in:slide={{ duration: 200, axis: 'x' }}>
 				<div
-					class="w-full h-full {$showOverview && !$showCallOverlay
+					class="w-full {$showOverview && !$showCallOverlay
 						? ' '
 						: 'px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850  border border-gray-50 dark:border-gray-800'}  rounded-lg z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
 				>
@@ -149,6 +173,6 @@
 					{/if}
 				</div>
 			</div>
-		</div>
+		</Pane>
 	{/if}
 </SvelteFlowProvider>

+ 11 - 2
src/lib/components/layout/Navbar.svelte

@@ -29,6 +29,7 @@
 	export let initNewChat: Function;
 	export let title: string = $WEBUI_NAME;
 	export let shareEnabled: boolean = false;
+	export let controlPane;
 
 	export let chat;
 	export let selectedModels;
@@ -109,8 +110,16 @@
 					<Tooltip content={$i18n.t('Controls')}>
 						<button
 							class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
-							on:click={() => {
-								showControls.set(!$showControls);
+							on:click={async () => {
+								await showControls.set(!$showControls);
+
+								if (controlPane) {
+									if ($showControls) {
+										controlPane.resize(localStorage.getItem('chat-controls-size') || 40);
+									} else {
+										controlPane.resize(0);
+									}
+								}
 							}}
 							aria-label="Controls"
 						>