mirror of
https://github.com/open-webui/open-webui
synced 2024-11-24 21:13:59 +00:00
enh: width adjustable chat controls
This commit is contained in:
parent
657d443a3e
commit
692f04d457
12
package-lock.json
generated
12
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user