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", "katex": "^0.16.9",
"marked": "^9.1.0", "marked": "^9.1.0",
"mermaid": "^10.9.1", "mermaid": "^10.9.1",
"paneforge": "^0.0.6",
"pyodide": "^0.26.1", "pyodide": "^0.26.1",
"socket.io-client": "^4.2.0", "socket.io-client": "^4.2.0",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
@ -6986,6 +6987,17 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",

View File

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

View File

@ -2,6 +2,7 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import mermaid from 'mermaid'; import mermaid from 'mermaid';
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
import { getContext, onDestroy, onMount, tick } from 'svelte'; import { getContext, onDestroy, onMount, tick } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -26,7 +27,8 @@
showControls, showControls,
showCallOverlay, showCallOverlay,
currentChatPage, currentChatPage,
temporaryChatEnabled temporaryChatEnabled,
mobile
} from '$lib/stores'; } from '$lib/stores';
import { import {
convertMessagesToHistory, convertMessagesToHistory,
@ -64,12 +66,14 @@
import Navbar from '$lib/components/layout/Navbar.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte';
import ChatControls from './ChatControls.svelte'; import ChatControls from './ChatControls.svelte';
import EventConfirmDialog from '../common/ConfirmDialog.svelte'; import EventConfirmDialog from '../common/ConfirmDialog.svelte';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
const i18n: Writable<i18nType> = getContext('i18n'); const i18n: Writable<i18nType> = getContext('i18n');
export let chatIdProp = ''; export let chatIdProp = '';
let loaded = false; let loaded = false;
const eventTarget = new EventTarget(); const eventTarget = new EventTarget();
let controlPane;
let stopResponseFlag = false; let stopResponseFlag = false;
let autoScroll = true; let autoScroll = true;
@ -1760,16 +1764,15 @@
bind:selectedModels bind:selectedModels
bind:showModelSelector bind:showModelSelector
shareEnabled={messages.length > 0} shareEnabled={messages.length > 0}
{controlPane}
{chat} {chat}
{initNewChat} {initNewChat}
/> />
<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} {#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
<div <div class="absolute top-3 left-0 right-0 w-full z-20">
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"> <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} {#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
<Banner <Banner
@ -1793,11 +1796,9 @@
</div> </div>
{/if} {/if}
<div class="flex flex-col flex-auto z-10"> <div class="flex flex-col flex-auto z-10 w-full">
<div <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 class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden"
? 'lg:pr-[26rem]'
: ''}"
id="messages-container" id="messages-container"
bind:this={messagesContainerElement} bind:this={messagesContainerElement}
on:scroll={(e) => { on:scroll={(e) => {
@ -1826,7 +1827,7 @@
</div> </div>
</div> </div>
<div class={$showControls ? 'lg:pr-[26rem]' : ''}> <div class="">
<MessageInput <MessageInput
bind:files bind:files
bind:prompt bind:prompt
@ -1852,10 +1853,9 @@
/> />
</div> </div>
</div> </div>
</div> </Pane>
{/if}
<ChatControls <ChatControls
models={selectedModelIds.reduce((a, e, i, arr) => { models={selectedModelIds.reduce((a, e, i, arr) => {
const model = $models.find((m) => m.id === e); const model = $models.find((m) => m.id === e);
if (model) { if (model) {
@ -1867,10 +1867,14 @@
bind:chatFiles bind:chatFiles
bind:params bind:params
bind:files bind:files
bind:pane={controlPane}
{submitPrompt} {submitPrompt}
{stopResponse} {stopResponse}
{showMessage} {showMessage}
modelId={selectedModelIds?.at(0) ?? null} modelId={selectedModelIds?.at(0) ?? null}
chatId={$chatId} chatId={$chatId}
{eventTarget} {eventTarget}
/> />
</PaneGroup>
</div>
{/if}

View File

@ -10,6 +10,9 @@
import CallOverlay from './MessageInput/CallOverlay.svelte'; import CallOverlay from './MessageInput/CallOverlay.svelte';
import Drawer from '../common/Drawer.svelte'; import Drawer from '../common/Drawer.svelte';
import Overview from './Overview.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 history;
export let models = []; export let models = [];
@ -25,7 +28,9 @@
export let files; export let files;
export let modelId; export let modelId;
export let pane;
let largeScreen = false; let largeScreen = false;
onMount(() => { onMount(() => {
// listen to resize 1024px // listen to resize 1024px
const mediaQuery = window.matchMedia('(min-width: 1024px)'); const mediaQuery = window.matchMedia('(min-width: 1024px)');
@ -58,10 +63,19 @@
<SvelteFlowProvider> <SvelteFlowProvider>
{#if !largeScreen} {#if !largeScreen}
{#if $showCallOverlay} {#if $showControls}
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden"> <Drawer
show={$showControls}
on:close={() => {
showControls.set(false);
}}
>
<div <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" 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 <CallOverlay
bind:files bind:files
@ -75,16 +89,7 @@
}} }}
/> />
</div> </div>
</div> {:else if $showOverview}
{:else 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}
<Overview <Overview
{history} {history}
on:nodeclick={(e) => { on:nodeclick={(e) => {
@ -107,11 +112,30 @@
</div> </div>
</Drawer> </Drawer>
{/if} {/if}
{:else if $showControls} {:else}
<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none"> <!-- if $showControls -->
<div class="pr-4 pt-14 pb-8 w-[26rem] h-full" in:slide={{ duration: 200, axis: 'x' }}> <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 <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" : '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} {/if}
</div> </div>
</div> </div>
</div> </Pane>
{/if} {/if}
</SvelteFlowProvider> </SvelteFlowProvider>

View File

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