mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-16 06:15:23 +00:00
enh: multi model response tabbed display
This commit is contained in:
parent
0067cf6eff
commit
8b9c5c4c1e
2 changed files with 171 additions and 41 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
import { mobile, settings } from '$lib/stores';
|
import { mobile, models, settings } from '$lib/stores';
|
||||||
|
|
||||||
import { generateMoACompletion } from '$lib/apis';
|
import { generateMoACompletion } from '$lib/apis';
|
||||||
import { updateChatById } from '$lib/apis/chats';
|
import { updateChatById } from '$lib/apis/chats';
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
import Name from './Name.svelte';
|
import Name from './Name.svelte';
|
||||||
import Skeleton from './Skeleton.svelte';
|
import Skeleton from './Skeleton.svelte';
|
||||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
|
import ProfileImage from './ProfileImage.svelte';
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
dayjs.extend(localizedFormat);
|
dayjs.extend(localizedFormat);
|
||||||
|
|
||||||
|
|
@ -53,6 +55,8 @@
|
||||||
let groupedMessageIds = {};
|
let groupedMessageIds = {};
|
||||||
let groupedMessageIdsIdx = {};
|
let groupedMessageIdsIdx = {};
|
||||||
|
|
||||||
|
let selectedModelIdx = null;
|
||||||
|
|
||||||
let message = JSON.parse(JSON.stringify(history.messages[messageId]));
|
let message = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||||
$: if (history.messages) {
|
$: if (history.messages) {
|
||||||
if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
|
if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
|
||||||
|
|
@ -183,11 +187,30 @@
|
||||||
}
|
}
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
selectedModelIdx = history.messages[messageId]?.modelIdx;
|
||||||
|
|
||||||
console.log(groupedMessageIds, groupedMessageIdsIdx);
|
console.log(groupedMessageIds, groupedMessageIdsIdx);
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onGroupClick = async (_messageId, modelIdx) => {
|
||||||
|
if (messageId != _messageId) {
|
||||||
|
let currentMessageId = _messageId;
|
||||||
|
let messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||||
|
while (messageChildrenIds.length !== 0) {
|
||||||
|
currentMessageId = messageChildrenIds.at(-1);
|
||||||
|
messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||||
|
}
|
||||||
|
history.currentId = currentMessageId;
|
||||||
|
selectedModelIdx = modelIdx;
|
||||||
|
|
||||||
|
// await tick();
|
||||||
|
// await updateChat();
|
||||||
|
// triggerScroll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const mergeResponsesHandler = async () => {
|
const mergeResponsesHandler = async () => {
|
||||||
const responses = Object.keys(groupedMessageIds).map((modelIdx) => {
|
const responses = Object.keys(groupedMessageIds).map((modelIdx) => {
|
||||||
const { messageIds } = groupedMessageIds[modelIdx];
|
const { messageIds } = groupedMessageIds[modelIdx];
|
||||||
|
|
@ -217,37 +240,58 @@
|
||||||
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
||||||
id="responses-container-{chatId}-{parentMessage.id}"
|
id="responses-container-{chatId}-{parentMessage.id}"
|
||||||
>
|
>
|
||||||
{#each Object.keys(groupedMessageIds) as modelIdx}
|
{#if $settings?.displayMultiModelResponsesInTabs ?? false}
|
||||||
{#if groupedMessageIdsIdx[modelIdx] !== undefined && groupedMessageIds[modelIdx].messageIds.length > 0}
|
<div class="w-full">
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<div class=" flex w-full mb-4 border-b border-gray-200 dark:border-gray-850">
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<div
|
||||||
{@const _messageId =
|
class="flex gap-2 scrollbar-none overflow-x-auto w-fit text-center font-medium bg-transparent pt-1 text-sm"
|
||||||
groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
|
>
|
||||||
|
{#each Object.keys(groupedMessageIds) as modelIdx}
|
||||||
|
{#if groupedMessageIdsIdx[modelIdx] !== undefined && groupedMessageIds[modelIdx].messageIds.length > 0}
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
|
||||||
<div
|
{@const _messageId =
|
||||||
class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
|
groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
|
||||||
?.modelIdx == modelIdx
|
|
||||||
? `bg-gray-50 dark:bg-gray-850 border-gray-100 dark:border-gray-800 border-2 ${
|
{@const model = $models.find((m) => m.id === history.messages[_messageId]?.model)}
|
||||||
$mobile ? 'min-w-full' : 'min-w-80'
|
|
||||||
}`
|
<button
|
||||||
: `border-gray-100 dark:border-gray-850 border-dashed ${
|
class="min-w-fit {selectedModelIdx == modelIdx
|
||||||
$mobile ? 'min-w-full' : 'min-w-80'
|
? ' dark:border-gray-300 '
|
||||||
}`} transition-all p-5 rounded-2xl"
|
: ' opacity-35 border-transparent'} pb-1.5 px-2.5 transition border-b-2"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
if (messageId != _messageId) {
|
if (selectedModelIdx != modelIdx) {
|
||||||
let currentMessageId = _messageId;
|
selectedModelIdx = modelIdx;
|
||||||
let messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
}
|
||||||
while (messageChildrenIds.length !== 0) {
|
|
||||||
currentMessageId = messageChildrenIds.at(-1);
|
onGroupClick(_messageId, modelIdx);
|
||||||
messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
}}
|
||||||
}
|
>
|
||||||
history.currentId = currentMessageId;
|
<div class="flex items-center gap-1.5">
|
||||||
// await tick();
|
<!-- <ProfileImage
|
||||||
// await updateChat();
|
src={model?.info?.meta?.profile_image_url ??
|
||||||
// triggerScroll();
|
($i18n.language === 'dg-DG'
|
||||||
}
|
? `${WEBUI_BASE_URL}/doge.png`
|
||||||
}}
|
: `${WEBUI_BASE_URL}/favicon.png`)}
|
||||||
>
|
className={'size-5 assistant-message-profile-image'}
|
||||||
|
/> -->
|
||||||
|
|
||||||
|
<div class="-translate-y-[1px]">
|
||||||
|
{model ? `${model.name}` : history.messages[_messageId]?.model}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if selectedModelIdx !== null}
|
||||||
|
{@const _messageId =
|
||||||
|
groupedMessageIds[selectedModelIdx].messageIds[
|
||||||
|
groupedMessageIdsIdx[selectedModelIdx]
|
||||||
|
]}
|
||||||
{#key history.currentId}
|
{#key history.currentId}
|
||||||
{#if message}
|
{#if message}
|
||||||
<ResponseMessage
|
<ResponseMessage
|
||||||
|
|
@ -256,10 +300,10 @@
|
||||||
messageId={_messageId}
|
messageId={_messageId}
|
||||||
{selectedModels}
|
{selectedModels}
|
||||||
isLastMessage={true}
|
isLastMessage={true}
|
||||||
siblings={groupedMessageIds[modelIdx].messageIds}
|
siblings={groupedMessageIds[selectedModelIdx].messageIds}
|
||||||
gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}
|
gotoMessage={(message, messageIdx) => gotoMessage(selectedModelIdx, messageIdx)}
|
||||||
showPreviousMessage={() => showPreviousMessage(modelIdx)}
|
showPreviousMessage={() => showPreviousMessage(selectedModelIdx)}
|
||||||
showNextMessage={() => showNextMessage(modelIdx)}
|
showNextMessage={() => showNextMessage(selectedModelIdx)}
|
||||||
{setInputText}
|
{setInputText}
|
||||||
{updateChat}
|
{updateChat}
|
||||||
{editMessage}
|
{editMessage}
|
||||||
|
|
@ -272,17 +316,73 @@
|
||||||
regenerateResponse={async (message) => {
|
regenerateResponse={async (message) => {
|
||||||
regenerateResponse(message);
|
regenerateResponse(message);
|
||||||
await tick();
|
await tick();
|
||||||
groupedMessageIdsIdx[modelIdx] =
|
groupedMessageIdsIdx[selectedModelIdx] =
|
||||||
groupedMessageIds[modelIdx].messageIds.length - 1;
|
groupedMessageIds[selectedModelIdx].messageIds.length - 1;
|
||||||
}}
|
}}
|
||||||
{addMessages}
|
{addMessages}
|
||||||
{readOnly}
|
{readOnly}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
{/each}
|
{:else}
|
||||||
|
{#each Object.keys(groupedMessageIds) as modelIdx}
|
||||||
|
{#if groupedMessageIdsIdx[modelIdx] !== undefined && groupedMessageIds[modelIdx].messageIds.length > 0}
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
{@const _messageId =
|
||||||
|
groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
|
||||||
|
?.modelIdx == modelIdx
|
||||||
|
? `bg-gray-50 dark:bg-gray-850 border-gray-100 dark:border-gray-800 border-2 ${
|
||||||
|
$mobile ? 'min-w-full' : 'min-w-80'
|
||||||
|
}`
|
||||||
|
: `border-gray-100 dark:border-gray-850 border-dashed ${
|
||||||
|
$mobile ? 'min-w-full' : 'min-w-80'
|
||||||
|
}`} transition-all p-5 rounded-2xl"
|
||||||
|
on:click={async () => {
|
||||||
|
onGroupClick(_messageId, modelIdx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#key history.currentId}
|
||||||
|
{#if message}
|
||||||
|
<ResponseMessage
|
||||||
|
{chatId}
|
||||||
|
{history}
|
||||||
|
messageId={_messageId}
|
||||||
|
{selectedModels}
|
||||||
|
isLastMessage={true}
|
||||||
|
siblings={groupedMessageIds[modelIdx].messageIds}
|
||||||
|
gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}
|
||||||
|
showPreviousMessage={() => showPreviousMessage(modelIdx)}
|
||||||
|
showNextMessage={() => showNextMessage(modelIdx)}
|
||||||
|
{setInputText}
|
||||||
|
{updateChat}
|
||||||
|
{editMessage}
|
||||||
|
{saveMessage}
|
||||||
|
{rateMessage}
|
||||||
|
{deleteMessage}
|
||||||
|
{actionMessage}
|
||||||
|
{submitMessage}
|
||||||
|
{continueResponse}
|
||||||
|
regenerateResponse={async (message) => {
|
||||||
|
regenerateResponse(message);
|
||||||
|
await tick();
|
||||||
|
groupedMessageIdsIdx[modelIdx] =
|
||||||
|
groupedMessageIds[modelIdx].messageIds.length - 1;
|
||||||
|
}}
|
||||||
|
{addMessages}
|
||||||
|
{readOnly}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
|
|
@ -296,7 +396,7 @@
|
||||||
{#if history.messages[messageId]?.merged?.status}
|
{#if history.messages[messageId]?.merged?.status}
|
||||||
{@const message = history.messages[messageId]?.merged}
|
{@const message = history.messages[messageId]?.merged}
|
||||||
|
|
||||||
<div class="w-full rounded-xl pl-5 pr-2 py-2">
|
<div class="w-full rounded-xl pl-5 pr-2 py-2 mt-2">
|
||||||
<Name>
|
<Name>
|
||||||
{$i18n.t('Merged Response')}
|
{$i18n.t('Merged Response')}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
let highContrastMode = false;
|
let highContrastMode = false;
|
||||||
|
|
||||||
let detectArtifacts = true;
|
let detectArtifacts = true;
|
||||||
|
let displayMultiModelResponsesInTabs = false;
|
||||||
|
|
||||||
let richTextInput = true;
|
let richTextInput = true;
|
||||||
let showFormattingToolbar = false;
|
let showFormattingToolbar = false;
|
||||||
|
|
@ -155,6 +156,11 @@
|
||||||
saveSettings({ showEmojiInCall: showEmojiInCall });
|
saveSettings({ showEmojiInCall: showEmojiInCall });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleDisplayMultiModelResponsesInTabs = async () => {
|
||||||
|
displayMultiModelResponsesInTabs = !displayMultiModelResponsesInTabs;
|
||||||
|
saveSettings({ displayMultiModelResponsesInTabs });
|
||||||
|
};
|
||||||
|
|
||||||
const toggleVoiceInterruption = async () => {
|
const toggleVoiceInterruption = async () => {
|
||||||
voiceInterruption = !voiceInterruption;
|
voiceInterruption = !voiceInterruption;
|
||||||
saveSettings({ voiceInterruption: voiceInterruption });
|
saveSettings({ voiceInterruption: voiceInterruption });
|
||||||
|
|
@ -344,6 +350,7 @@
|
||||||
showEmojiInCall = $settings?.showEmojiInCall ?? false;
|
showEmojiInCall = $settings?.showEmojiInCall ?? false;
|
||||||
voiceInterruption = $settings?.voiceInterruption ?? false;
|
voiceInterruption = $settings?.voiceInterruption ?? false;
|
||||||
|
|
||||||
|
displayMultiModelResponsesInTabs = $settings?.displayMultiModelResponsesInTabs ?? false;
|
||||||
chatFadeStreamingText = $settings?.chatFadeStreamingText ?? true;
|
chatFadeStreamingText = $settings?.chatFadeStreamingText ?? true;
|
||||||
|
|
||||||
richTextInput = $settings?.richTextInput ?? true;
|
richTextInput = $settings?.richTextInput ?? true;
|
||||||
|
|
@ -853,6 +860,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div id="keep-followup-prompts-label" class=" self-center text-xs">
|
||||||
|
{$i18n.t('Display Multi-model Responses in Tabs')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
aria-labelledby="keep-followup-prompts-label"
|
||||||
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||||
|
on:click={() => {
|
||||||
|
toggleDisplayMultiModelResponsesInTabs();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if displayMultiModelResponsesInTabs === true}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div id="rich-input-label" class=" self-center text-xs">
|
<div id="rich-input-label" class=" self-center text-xs">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue