open-webui/src/lib/components/chat/Messages.svelte
Timothy J. Baek 398bc96b1a refac
2024-09-24 01:05:46 +02:00

407 lines
11 KiB
Svelte

<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores';
import { tick, getContext, onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats';
import { copyToClipboard, findWordIndices } from '$lib/utils';
import Placeholder from './Messages/Placeholder.svelte';
import Message from './Messages/Message.svelte';
import Loader from '../common/Loader.svelte';
import Spinner from '../common/Spinner.svelte';
const i18n = getContext('i18n');
export let chatId = '';
export let user = $_user;
export let prompt;
export let history = {};
export let selectedModels;
let messages = [];
export let sendPrompt: Function;
export let continueResponse: Function;
export let regenerateResponse: Function;
export let mergeResponses: Function;
export let chatActionHandler: Function;
export let showMessage: Function = () => {};
export let readOnly = false;
export let bottomPadding = false;
export let autoScroll;
let messagesCount = 20;
let messagesLoading = false;
const loadMoreMessages = async () => {
// scroll slightly down to disable continuous loading
const element = document.getElementById('messages-container');
element.scrollTop = element.scrollTop + 100;
messagesLoading = true;
messagesCount += 20;
await tick();
messagesLoading = false;
};
$: if (history.currentId) {
let _messages = [];
let message = history.messages[history.currentId];
while (message && _messages.length <= messagesCount) {
_messages.unshift({ ...message });
message = message.parentId !== null ? history.messages[message.parentId] : null;
}
messages = _messages;
} else {
messages = [];
}
$: if (autoScroll && bottomPadding) {
(async () => {
await tick();
scrollToBottom();
})();
}
const scrollToBottom = () => {
const element = document.getElementById('messages-container');
element.scrollTop = element.scrollHeight;
};
const updateChatHistory = async () => {
await tick();
await updateChatById(localStorage.token, chatId, {
history: history
});
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
};
const showPreviousMessage = async (message) => {
if (message.parentId !== null) {
let messageId =
history.messages[message.parentId].childrenIds[
Math.max(history.messages[message.parentId].childrenIds.indexOf(message.id) - 1, 0)
];
if (message.id !== messageId) {
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
}
} else {
let childrenIds = Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id);
let messageId = childrenIds[Math.max(childrenIds.indexOf(message.id) - 1, 0)];
if (message.id !== messageId) {
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
}
}
await tick();
if ($settings?.scrollOnBranchChange ?? true) {
const element = document.getElementById('messages-container');
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
setTimeout(() => {
scrollToBottom();
}, 100);
}
};
const showNextMessage = async (message) => {
if (message.parentId !== null) {
let messageId =
history.messages[message.parentId].childrenIds[
Math.min(
history.messages[message.parentId].childrenIds.indexOf(message.id) + 1,
history.messages[message.parentId].childrenIds.length - 1
)
];
if (message.id !== messageId) {
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
}
} else {
let childrenIds = Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id);
let messageId =
childrenIds[Math.min(childrenIds.indexOf(message.id) + 1, childrenIds.length - 1)];
if (message.id !== messageId) {
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
}
}
await tick();
if ($settings?.scrollOnBranchChange ?? true) {
const element = document.getElementById('messages-container');
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
setTimeout(() => {
scrollToBottom();
}, 100);
}
};
const rateMessage = async (messageId, rating) => {
history.messages[messageId].annotation = {
...history.messages[messageId].annotation,
rating: rating
};
await updateChatHistory();
};
const editMessage = async (messageId, content, submit = true) => {
if (history.messages[messageId].role === 'user') {
if (submit) {
// New user message
let userPrompt = content;
let userMessageId = uuidv4();
let userMessage = {
id: userMessageId,
parentId: history.messages[messageId].parentId,
childrenIds: [],
role: 'user',
content: userPrompt,
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
models: selectedModels
};
let messageParentId = history.messages[messageId].parentId;
if (messageParentId !== null) {
history.messages[messageParentId].childrenIds = [
...history.messages[messageParentId].childrenIds,
userMessageId
];
}
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
await tick();
await sendPrompt(userPrompt, userMessageId);
} else {
// Edit user message
history.messages[messageId].content = content;
await updateChatHistory();
}
} else {
if (submit) {
// New response message
const responseMessageId = uuidv4();
const message = history.messages[messageId];
const parentId = message.parentId;
const responseMessage = {
...message,
id: responseMessageId,
parentId: parentId,
childrenIds: [],
content: content,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
};
history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId;
// Append messageId to childrenIds of parent message
if (parentId !== null) {
history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds,
responseMessageId
];
}
await updateChatHistory();
} else {
// Edit response message
history.messages[messageId].originalContent = history.messages[messageId].content;
history.messages[messageId].content = content;
await updateChatHistory();
}
}
};
const deleteMessage = async (messageId) => {
const messageToDelete = history.messages[messageId];
const parentMessageId = messageToDelete.parentId;
const childMessageIds = messageToDelete.childrenIds ?? [];
// Collect all grandchildren
const grandchildrenIds = childMessageIds.flatMap(
(childId) => history.messages[childId]?.childrenIds ?? []
);
// Update parent's children
if (parentMessageId && history.messages[parentMessageId]) {
history.messages[parentMessageId].childrenIds = [
...history.messages[parentMessageId].childrenIds.filter((id) => id !== messageId),
...grandchildrenIds
];
}
// Update grandchildren's parent
grandchildrenIds.forEach((grandchildId) => {
if (history.messages[grandchildId]) {
history.messages[grandchildId].parentId = parentMessageId;
}
});
// Delete the message and its children
[messageId, ...childMessageIds].forEach((id) => {
delete history.messages[id];
});
await tick();
showMessage({ id: parentMessageId });
// Update the chat
await updateChatHistory();
};
</script>
<div class="h-full flex">
{#if Object.keys(history?.messages ?? {}).length == 0}
<Placeholder
modelIds={selectedModels}
submitPrompt={async (p) => {
let text = p;
if (p.includes('{{CLIPBOARD}}')) {
const clipboardText = await navigator.clipboard.readText().catch((err) => {
toast.error($i18n.t('Failed to read clipboard contents'));
return '{{CLIPBOARD}}';
});
text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
}
prompt = text;
await tick();
const chatInputElement = document.getElementById('chat-textarea');
if (chatInputElement) {
prompt = p;
chatInputElement.style.height = '';
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
chatInputElement.focus();
const words = findWordIndices(prompt);
if (words.length > 0) {
const word = words.at(0);
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
}
await tick();
}}
/>
{:else}
<div class="w-full pt-2">
{#key chatId}
<div class="w-full">
{#if messages.at(0)?.parentId !== null}
<Loader
on:visible={(e) => {
console.log('visible');
if (!messagesLoading) {
loadMoreMessages();
}
}}
>
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
</Loader>
{/if}
{#each messages as message, messageIdx (message.id)}
<Message
{chatId}
bind:history
messageId={message.id}
idx={messageIdx}
{user}
{showPreviousMessage}
{showNextMessage}
{editMessage}
{deleteMessage}
{rateMessage}
{regenerateResponse}
{continueResponse}
{mergeResponses}
{updateChatHistory}
{chatActionHandler}
{readOnly}
on:scroll={() => {
if (autoScroll) {
const element = document.getElementById('messages-container');
autoScroll =
element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
setTimeout(() => {
scrollToBottom();
}, 100);
}
}}
/>
{/each}
</div>
<div class="pb-12" />
{#if bottomPadding}
<div class=" pb-6" />
{/if}
{/key}
</div>
{/if}
</div>