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",
|
"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",
|
||||||
|
@ -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",
|
||||||
|
@ -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,117 +1764,117 @@
|
|||||||
bind:selectedModels
|
bind:selectedModels
|
||||||
bind:showModelSelector
|
bind:showModelSelector
|
||||||
shareEnabled={messages.length > 0}
|
shareEnabled={messages.length > 0}
|
||||||
|
{controlPane}
|
||||||
{chat}
|
{chat}
|
||||||
{initNewChat}
|
{initNewChat}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
|
<PaneGroup direction="horizontal" class="w-full h-full">
|
||||||
<div
|
<Pane defaultSize={50} class="h-full flex w-full relative">
|
||||||
class="absolute top-[4.25rem] w-full {$showSidebar
|
{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
|
||||||
? 'md:max-w-[calc(100%-260px)]'
|
<div class="absolute top-3 left-0 right-0 w-full z-20">
|
||||||
: ''} {$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}
|
||||||
<div class=" flex flex-col gap-1 w-full">
|
<Banner
|
||||||
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
{banner}
|
||||||
<Banner
|
on:dismiss={(e) => {
|
||||||
{banner}
|
const bannerId = e.detail;
|
||||||
on:dismiss={(e) => {
|
|
||||||
const bannerId = e.detail;
|
|
||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'dismissedBannerIds',
|
'dismissedBannerIds',
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
[
|
[
|
||||||
bannerId,
|
bannerId,
|
||||||
...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
|
...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
|
||||||
].filter((id) => $banners.find((b) => b.id === id))
|
].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>
|
||||||
</div>
|
</Pane>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex flex-col flex-auto z-10">
|
<ChatControls
|
||||||
<div
|
models={selectedModelIds.reduce((a, e, i, arr) => {
|
||||||
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
|
const model = $models.find((m) => m.id === e);
|
||||||
? 'lg:pr-[26rem]'
|
if (model) {
|
||||||
: ''}"
|
return [...a, model];
|
||||||
id="messages-container"
|
}
|
||||||
bind:this={messagesContainerElement}
|
return a;
|
||||||
on:scroll={(e) => {
|
}, [])}
|
||||||
autoScroll =
|
bind:history
|
||||||
messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
|
bind:chatFiles
|
||||||
messagesContainerElement.clientHeight + 5;
|
bind:params
|
||||||
}}
|
bind:files
|
||||||
>
|
bind:pane={controlPane}
|
||||||
<div class=" h-full w-full flex flex-col {chatIdProp ? 'py-4' : 'pt-2 pb-4'}">
|
{submitPrompt}
|
||||||
<Messages
|
{stopResponse}
|
||||||
chatId={$chatId}
|
{showMessage}
|
||||||
{selectedModels}
|
modelId={selectedModelIds?.at(0) ?? null}
|
||||||
{processing}
|
chatId={$chatId}
|
||||||
bind:history
|
{eventTarget}
|
||||||
bind:messages
|
/>
|
||||||
bind:autoScroll
|
</PaneGroup>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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 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,33 +63,33 @@
|
|||||||
|
|
||||||
<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">
|
|
||||||
<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}
|
|
||||||
<Drawer
|
<Drawer
|
||||||
show={$showControls}
|
show={$showControls}
|
||||||
on:close={() => {
|
on:close={() => {
|
||||||
showControls.set(false);
|
showControls.set(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" {$showOverview ? ' h-screen w-screen' : 'px-6 py-4'} h-full">
|
<div
|
||||||
{#if $showOverview}
|
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
|
<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>
|
||||||
|
@ -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"
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user