| | <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, createEventDispatcher } from 'svelte'; |
| | const dispatch = createEventDispatcher(); |
| | |
| | import { toast } from 'svelte-sonner'; |
| | import { getChatList, updateChatById } from '$lib/apis/chats'; |
| | import { copyToClipboard, findWordIndices } from '$lib/utils'; |
| | |
| | import Message from './Messages/Message.svelte'; |
| | import Loader from '../common/Loader.svelte'; |
| | import Spinner from '../common/Spinner.svelte'; |
| | |
| | import ChatPlaceholder from './ChatPlaceholder.svelte'; |
| | |
| | const i18n = getContext('i18n'); |
| | |
| | export let className = 'h-full flex pt-8'; |
| | |
| | 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 submitMessage: Function = () => {}; |
| | export let addMessages: 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 updateChat = async () => { |
| | history = history; |
| | await tick(); |
| | await updateChatById(localStorage.token, chatId, { |
| | history: history, |
| | messages: messages |
| | }); |
| | |
| | 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 updateChat(); |
| | }; |
| | |
| | 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 updateChat(); |
| | } |
| | } 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: [], |
| | files: undefined, |
| | content: content, |
| | timestamp: Math.floor(Date.now() / 1000) // Unix epoch |
| | }; |
| | |
| | history.messages[responseMessageId] = responseMessage; |
| | history.currentId = responseMessageId; |
| | |
| | |
| | if (parentId !== null) { |
| | history.messages[parentId].childrenIds = [ |
| | ...history.messages[parentId].childrenIds, |
| | responseMessageId |
| | ]; |
| | } |
| | |
| | await updateChat(); |
| | } else { |
| | // Edit response message |
| | history.messages[messageId].originalContent = history.messages[messageId].content; |
| | history.messages[messageId].content = content; |
| | await updateChat(); |
| | } |
| | } |
| | }; |
| | |
| | const actionMessage = async (actionId, message, event = null) => { |
| | await chatActionHandler(chatId, actionId, message.model, message.id, event); |
| | }; |
| | |
| | const saveMessage = async (messageId, message) => { |
| | history.messages[messageId] = message; |
| | await updateChat(); |
| | }; |
| | |
| | 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 |
| | ]; |
| | } |
| | |
| | |
| | grandchildrenIds.forEach((grandchildId) => { |
| | if (history.messages[grandchildId]) { |
| | history.messages[grandchildId].parentId = parentMessageId; |
| | } |
| | }); |
| | |
| | |
| | [messageId, ...childMessageIds].forEach((id) => { |
| | delete history.messages[id]; |
| | }); |
| | |
| | await tick(); |
| | |
| | showMessage({ id: parentMessageId }); |
| | |
| | |
| | await updateChat(); |
| | }; |
| | |
| | const triggerScroll = () => { |
| | if (autoScroll) { |
| | const element = document.getElementById('messages-container'); |
| | autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50; |
| | setTimeout(() => { |
| | scrollToBottom(); |
| | }, 100); |
| | } |
| | }; |
| | </script> |
| |
|
| | <div class={className}> |
| | {#if Object.keys(history?.messages ?? {}).length == 0} |
| | <ChatPlaceholder |
| | 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 chatInputContainerElement = document.getElementById('chat-input-container'); |
| | if (chatInputContainerElement) { |
| | prompt = p; |
| |
|
| | chatInputContainerElement.style.height = ''; |
| | chatInputContainerElement.style.height = |
| | Math.min(chatInputContainerElement.scrollHeight, 200) + 'px'; |
| | chatInputContainerElement.focus(); |
| | } |
| |
|
| | 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} |
| | {updateChat} |
| | {editMessage} |
| | {deleteMessage} |
| | {rateMessage} |
| | {actionMessage} |
| | {saveMessage} |
| | {submitMessage} |
| | {regenerateResponse} |
| | {continueResponse} |
| | {mergeResponses} |
| | {addMessages} |
| | {triggerScroll} |
| | {readOnly} |
| | /> |
| | {/each} |
| | </div> |
| | <div class="pb-12" /> |
| | {#if bottomPadding} |
| | <div class=" pb-6" /> |
| | {/if} |
| | {/key} |
| | </div> |
| | {/if} |
| | </div> |
| |
|