enh: width adjustable chat controls

This commit is contained in:
Timothy J. Baek 2024-09-21 03:33:06 +02:00
parent 657d443a3e
commit 692f04d457
5 changed files with 182 additions and 132 deletions

12
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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;
<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))
)
);
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}
/>

View File

@ -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>

View File

@ -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"
>