mirror of
https://github.com/open-webui/open-webui
synced 2025-06-11 00:49:44 +00:00
feat: many model interaction ui
This commit is contained in:
parent
0ede1d1483
commit
676a4dffd0
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { chats, config, modelfiles, settings, user as _user } from '$lib/stores';
|
import { chats, config, modelfiles, settings, user as _user, mobile } from '$lib/stores';
|
||||||
import { tick, getContext } from 'svelte';
|
import { tick, getContext } from 'svelte';
|
||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
@ -13,6 +13,8 @@
|
|||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import { imageGenerations } from '$lib/apis/images';
|
import { imageGenerations } from '$lib/apis/images';
|
||||||
import { copyToClipboard, findWordIndices } from '$lib/utils';
|
import { copyToClipboard, findWordIndices } from '$lib/utils';
|
||||||
|
import CompareMessages from './Messages/CompareMessages.svelte';
|
||||||
|
import { stringify } from 'postcss';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@ -28,10 +30,10 @@
|
|||||||
export let processing = '';
|
export let processing = '';
|
||||||
export let bottomPadding = false;
|
export let bottomPadding = false;
|
||||||
export let autoScroll;
|
export let autoScroll;
|
||||||
export let selectedModels;
|
|
||||||
export let history = {};
|
export let history = {};
|
||||||
export let messages = [];
|
export let messages = [];
|
||||||
|
|
||||||
|
export let selectedModels;
|
||||||
export let selectedModelfiles = [];
|
export let selectedModelfiles = [];
|
||||||
|
|
||||||
$: if (autoScroll && bottomPadding) {
|
$: if (autoScroll && bottomPadding) {
|
||||||
@ -63,7 +65,8 @@
|
|||||||
childrenIds: [],
|
childrenIds: [],
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: userPrompt,
|
content: userPrompt,
|
||||||
...(history.messages[messageId].files && { files: history.messages[messageId].files })
|
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
|
||||||
|
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
|
||||||
};
|
};
|
||||||
|
|
||||||
let messageParentId = history.messages[messageId].parentId;
|
let messageParentId = history.messages[messageId].parentId;
|
||||||
@ -79,7 +82,7 @@
|
|||||||
history.currentId = userMessageId;
|
history.currentId = userMessageId;
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
await sendPrompt(userPrompt, userMessageId, chatId);
|
await sendPrompt(userPrompt, userMessageId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateChatMessages = async () => {
|
const updateChatMessages = async () => {
|
||||||
@ -309,7 +312,7 @@
|
|||||||
{showNextMessage}
|
{showNextMessage}
|
||||||
copyToClipboard={copyToClipboardWithToast}
|
copyToClipboard={copyToClipboardWithToast}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
|
||||||
{#key message.id}
|
{#key message.id}
|
||||||
<ResponseMessage
|
<ResponseMessage
|
||||||
{message}
|
{message}
|
||||||
@ -337,6 +340,32 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
{:else}
|
||||||
|
{#key message.parentId}
|
||||||
|
<CompareMessages
|
||||||
|
bind:history
|
||||||
|
{messages}
|
||||||
|
{chatId}
|
||||||
|
parentMessage={history.messages[message.parentId]}
|
||||||
|
{messageIdx}
|
||||||
|
{selectedModelfiles}
|
||||||
|
{updateChatMessages}
|
||||||
|
{confirmEditResponseMessage}
|
||||||
|
{rateMessage}
|
||||||
|
copyToClipboard={copyToClipboardWithToast}
|
||||||
|
{continueGeneration}
|
||||||
|
{regenerateResponse}
|
||||||
|
on:change={() => {
|
||||||
|
const element = document.getElementById('messages-container');
|
||||||
|
autoScroll =
|
||||||
|
element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, 100);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
156
src/lib/components/chat/Messages/CompareMessages.svelte
Normal file
156
src/lib/components/chat/Messages/CompareMessages.svelte
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import { updateChatById } from '$lib/apis/chats';
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
import ResponseMessage from './ResponseMessage.svelte';
|
||||||
|
|
||||||
|
export let chatId;
|
||||||
|
|
||||||
|
export let history;
|
||||||
|
export let messages = [];
|
||||||
|
export let messageIdx;
|
||||||
|
|
||||||
|
export let parentMessage;
|
||||||
|
|
||||||
|
export let selectedModelfiles;
|
||||||
|
|
||||||
|
export let updateChatMessages: Function;
|
||||||
|
export let confirmEditResponseMessage: Function;
|
||||||
|
export let rateMessage: Function;
|
||||||
|
|
||||||
|
export let copyToClipboard: Function;
|
||||||
|
export let continueGeneration: Function;
|
||||||
|
export let regenerateResponse: Function;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let currentMessageId;
|
||||||
|
|
||||||
|
let groupedMessagesIdx = {};
|
||||||
|
let groupedMessages = {};
|
||||||
|
|
||||||
|
$: groupedMessages = parentMessage?.models.reduce((a, model) => {
|
||||||
|
const modelMessages = parentMessage?.childrenIds
|
||||||
|
.map((id) => history.messages[id])
|
||||||
|
.filter((m) => m.model === model);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...a,
|
||||||
|
[model]: { messages: modelMessages }
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await tick();
|
||||||
|
currentMessageId = messages[messageIdx].id;
|
||||||
|
|
||||||
|
for (const model of parentMessage?.models) {
|
||||||
|
const idx = groupedMessages[model].messages.findIndex((m) => m.id === currentMessageId);
|
||||||
|
|
||||||
|
if (idx !== -1) {
|
||||||
|
groupedMessagesIdx[model] = idx;
|
||||||
|
} else {
|
||||||
|
groupedMessagesIdx[model] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex snap-x snap-mandatory overflow-x-auto scrollbar-none">
|
||||||
|
{#each Object.keys(groupedMessages) as model}
|
||||||
|
{#if groupedMessagesIdx[model] !== undefined && groupedMessages[model].messages.length > 0}
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class=" snap-center min-w-96 w-full max-w-full m-1 outline outline-1 {history.messages[
|
||||||
|
currentMessageId
|
||||||
|
].model === model
|
||||||
|
? 'outline-gray-200 dark:outline-gray-700 outline-2'
|
||||||
|
: 'outline-gray-100 dark:outline-gray-850 '} transition p-6 rounded-3xl"
|
||||||
|
on:click={() => {
|
||||||
|
currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||||
|
|
||||||
|
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||||
|
|
||||||
|
console.log(messageId);
|
||||||
|
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||||
|
|
||||||
|
while (messageChildrenIds.length !== 0) {
|
||||||
|
messageId = messageChildrenIds.at(-1);
|
||||||
|
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.currentId = messageId;
|
||||||
|
dispatch('change');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ResponseMessage
|
||||||
|
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
|
||||||
|
modelfiles={selectedModelfiles}
|
||||||
|
siblings={groupedMessages[model].messages.map((m) => m.id)}
|
||||||
|
isLastMessage={true}
|
||||||
|
{updateChatMessages}
|
||||||
|
{confirmEditResponseMessage}
|
||||||
|
showPreviousMessage={() => {
|
||||||
|
groupedMessagesIdx[model] = Math.max(0, groupedMessagesIdx[model] - 1);
|
||||||
|
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||||
|
|
||||||
|
console.log(messageId);
|
||||||
|
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||||
|
|
||||||
|
while (messageChildrenIds.length !== 0) {
|
||||||
|
messageId = messageChildrenIds.at(-1);
|
||||||
|
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.currentId = messageId;
|
||||||
|
|
||||||
|
dispatch('change');
|
||||||
|
}}
|
||||||
|
showNextMessage={() => {
|
||||||
|
groupedMessagesIdx[model] = Math.min(
|
||||||
|
groupedMessages[model].messages.length - 1,
|
||||||
|
groupedMessagesIdx[model] + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||||
|
console.log(messageId);
|
||||||
|
|
||||||
|
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||||
|
|
||||||
|
while (messageChildrenIds.length !== 0) {
|
||||||
|
messageId = messageChildrenIds.at(-1);
|
||||||
|
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.currentId = messageId;
|
||||||
|
|
||||||
|
dispatch('change');
|
||||||
|
}}
|
||||||
|
{rateMessage}
|
||||||
|
{copyToClipboard}
|
||||||
|
{continueGeneration}
|
||||||
|
regenerateResponse={async (model) => {
|
||||||
|
regenerateResponse(model);
|
||||||
|
await tick();
|
||||||
|
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
|
||||||
|
}}
|
||||||
|
on:save={async (e) => {
|
||||||
|
console.log('save', e);
|
||||||
|
|
||||||
|
const message = e.detail;
|
||||||
|
history.messages[message.id] = message;
|
||||||
|
await updateChatById(localStorage.token, chatId, {
|
||||||
|
messages: messages,
|
||||||
|
history: history
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
let selectedCitation = null;
|
let selectedCitation = null;
|
||||||
|
|
||||||
$: tokens = marked.lexer(sanitizeResponseContent(message.content));
|
$: tokens = marked.lexer(sanitizeResponseContent(message?.content));
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
@ -499,7 +499,7 @@
|
|||||||
class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
|
class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
|
||||||
>
|
>
|
||||||
{#if siblings.length > 1}
|
{#if siblings.length > 1}
|
||||||
<div class="flex self-center" dir="ltr">
|
<div class="flex self-center min-w-fit" dir="ltr">
|
||||||
<button
|
<button
|
||||||
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@ -523,7 +523,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"
|
class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit"
|
||||||
>
|
>
|
||||||
{siblings.indexOf(message.id) + 1}/{siblings.length}
|
{siblings.indexOf(message.id) + 1}/{siblings.length}
|
||||||
</div>
|
</div>
|
||||||
@ -894,7 +894,9 @@
|
|||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||||
on:click={regenerateResponse}
|
on:click={() => {
|
||||||
|
regenerateResponse(message.model);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -202,6 +202,7 @@
|
|||||||
user: _user ?? undefined,
|
user: _user ?? undefined,
|
||||||
content: userPrompt,
|
content: userPrompt,
|
||||||
files: files.length > 0 ? files : undefined,
|
files: files.length > 0 ? files : undefined,
|
||||||
|
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx),
|
||||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -250,11 +251,12 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPrompt = async (prompt, parentId) => {
|
const sendPrompt = async (prompt, parentId, modelId = null) => {
|
||||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
(atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(async (modelId) => {
|
(modelId ? [modelId] : atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(
|
||||||
|
async (modelId) => {
|
||||||
console.log('modelId', modelId);
|
console.log('modelId', modelId);
|
||||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||||
|
|
||||||
@ -291,7 +293,8 @@
|
|||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
|
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await chats.set(await getChatList(localStorage.token));
|
await chats.set(await getChatList(localStorage.token));
|
||||||
@ -756,7 +759,7 @@
|
|||||||
console.log('stopResponse');
|
console.log('stopResponse');
|
||||||
};
|
};
|
||||||
|
|
||||||
const regenerateResponse = async () => {
|
const regenerateResponse = async (modelId) => {
|
||||||
console.log('regenerateResponse');
|
console.log('regenerateResponse');
|
||||||
if (messages.length != 0 && messages.at(-1).done == true) {
|
if (messages.length != 0 && messages.at(-1).done == true) {
|
||||||
messages.splice(messages.length - 1, 1);
|
messages.splice(messages.length - 1, 1);
|
||||||
@ -765,7 +768,7 @@
|
|||||||
let userMessage = messages.at(-1);
|
let userMessage = messages.at(-1);
|
||||||
let userPrompt = userMessage.content;
|
let userPrompt = userMessage.content;
|
||||||
|
|
||||||
await sendPrompt(userPrompt, userMessage.id);
|
await sendPrompt(userPrompt, userMessage.id, modelId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -210,7 +210,8 @@
|
|||||||
user: _user ?? undefined,
|
user: _user ?? undefined,
|
||||||
content: userPrompt,
|
content: userPrompt,
|
||||||
files: files.length > 0 ? files : undefined,
|
files: files.length > 0 ? files : undefined,
|
||||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
timestamp: Math.floor(Date.now() / 1000), // Unix epoch
|
||||||
|
models: selectedModels
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add message to history and Set currentId to messageId
|
// Add message to history and Set currentId to messageId
|
||||||
@ -255,11 +256,14 @@
|
|||||||
await sendPrompt(userPrompt, userMessageId);
|
await sendPrompt(userPrompt, userMessageId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const sendPrompt = async (prompt, parentId) => {
|
|
||||||
|
const sendPrompt = async (prompt, parentId, modelId = null) => {
|
||||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
(atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(async (modelId) => {
|
(modelId ? [modelId] : atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(
|
||||||
|
async (modelId) => {
|
||||||
|
console.log('modelId', modelId);
|
||||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||||
|
|
||||||
if (model) {
|
if (model) {
|
||||||
@ -295,7 +299,8 @@
|
|||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
|
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await chats.set(await getChatList(localStorage.token));
|
await chats.set(await getChatList(localStorage.token));
|
||||||
@ -759,7 +764,7 @@
|
|||||||
console.log('stopResponse');
|
console.log('stopResponse');
|
||||||
};
|
};
|
||||||
|
|
||||||
const regenerateResponse = async () => {
|
const regenerateResponse = async (modelId = null) => {
|
||||||
console.log('regenerateResponse');
|
console.log('regenerateResponse');
|
||||||
if (messages.length != 0 && messages.at(-1).done == true) {
|
if (messages.length != 0 && messages.at(-1).done == true) {
|
||||||
messages.splice(messages.length - 1, 1);
|
messages.splice(messages.length - 1, 1);
|
||||||
@ -768,7 +773,7 @@
|
|||||||
let userMessage = messages.at(-1);
|
let userMessage = messages.at(-1);
|
||||||
let userPrompt = userMessage.content;
|
let userPrompt = userMessage.content;
|
||||||
|
|
||||||
await sendPrompt(userPrompt, userMessage.id);
|
await sendPrompt(userPrompt, userMessage.id, modelId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user