|
<script lang="ts"> |
|
import { v4 as uuidv4 } from 'uuid'; |
|
import { chats, config, settings, user as _user, mobile } from '$lib/stores'; |
|
import { tick, getContext, onMount } from 'svelte'; |
|
|
|
import { toast } from 'svelte-sonner'; |
|
import { getChatList, updateChatById } from '$lib/apis/chats'; |
|
|
|
import UserMessage from './Messages/UserMessage.svelte'; |
|
import ResponseMessage from './Messages/ResponseMessage.svelte'; |
|
import Placeholder from './Messages/Placeholder.svelte'; |
|
import Spinner from '../common/Spinner.svelte'; |
|
import { imageGenerations } from '$lib/apis/images'; |
|
import { copyToClipboard, findWordIndices } from '$lib/utils'; |
|
import CompareMessages from './Messages/CompareMessages.svelte'; |
|
import { stringify } from 'postcss'; |
|
|
|
const i18n = getContext('i18n'); |
|
|
|
export let chatId = ''; |
|
export let readOnly = false; |
|
export let sendPrompt: Function; |
|
export let continueGeneration: Function; |
|
export let regenerateResponse: Function; |
|
export let chatActionHandler: Function; |
|
|
|
export let user = $_user; |
|
export let prompt; |
|
export let processing = ''; |
|
export let bottomPadding = false; |
|
export let autoScroll; |
|
export let history = {}; |
|
export let messages = []; |
|
|
|
export let selectedModels; |
|
|
|
$: if (autoScroll && bottomPadding) { |
|
(async () => { |
|
await tick(); |
|
scrollToBottom(); |
|
})(); |
|
} |
|
|
|
const scrollToBottom = () => { |
|
const element = document.getElementById('messages-container'); |
|
element.scrollTop = element.scrollHeight; |
|
}; |
|
|
|
const copyToClipboardWithToast = async (text) => { |
|
const res = await copyToClipboard(text); |
|
if (res) { |
|
toast.success($i18n.t('Copying to clipboard was successful!')); |
|
} |
|
}; |
|
|
|
const confirmEditMessage = async (messageId, content) => { |
|
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.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx) |
|
}; |
|
|
|
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); |
|
}; |
|
|
|
const updateChatMessages = async () => { |
|
await tick(); |
|
await updateChatById(localStorage.token, chatId, { |
|
messages: messages, |
|
history: history |
|
}); |
|
|
|
await chats.set(await getChatList(localStorage.token)); |
|
}; |
|
|
|
const confirmEditResponseMessage = async (messageId, content) => { |
|
history.messages[messageId].originalContent = history.messages[messageId].content; |
|
history.messages[messageId].content = content; |
|
|
|
await updateChatMessages(); |
|
}; |
|
|
|
const rateMessage = async (messageId, rating) => { |
|
history.messages[messageId].annotation = { |
|
...history.messages[messageId].annotation, |
|
rating: rating |
|
}; |
|
|
|
await updateChatMessages(); |
|
}; |
|
|
|
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(); |
|
|
|
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(); |
|
|
|
const element = document.getElementById('messages-container'); |
|
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50; |
|
|
|
setTimeout(() => { |
|
scrollToBottom(); |
|
}, 100); |
|
}; |
|
|
|
const deleteMessageHandler = async (messageId) => { |
|
const messageToDelete = history.messages[messageId]; |
|
|
|
const parentMessageId = messageToDelete.parentId; |
|
const childMessageIds = messageToDelete.childrenIds ?? []; |
|
|
|
const hasDescendantMessages = childMessageIds.some( |
|
(childId) => history.messages[childId]?.childrenIds?.length > 0 |
|
); |
|
|
|
history.currentId = parentMessageId; |
|
await tick(); |
|
|
|
// Remove the message itself from the parent message's children array |
|
history.messages[parentMessageId].childrenIds = history.messages[ |
|
parentMessageId |
|
].childrenIds.filter((id) => id !== messageId); |
|
|
|
await tick(); |
|
|
|
childMessageIds.forEach((childId) => { |
|
const childMessage = history.messages[childId]; |
|
|
|
if (childMessage && childMessage.childrenIds) { |
|
if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) { |
|
// If there are no other responses/prompts |
|
history.messages[parentMessageId].childrenIds = []; |
|
} else { |
|
childMessage.childrenIds.forEach((grandChildId) => { |
|
if (history.messages[grandChildId]) { |
|
history.messages[grandChildId].parentId = parentMessageId; |
|
history.messages[parentMessageId].childrenIds.push(grandChildId); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
history.messages[parentMessageId].childrenIds = history.messages[ |
|
parentMessageId |
|
].childrenIds.filter((id) => id !== childId); |
|
}); |
|
|
|
await tick(); |
|
|
|
await updateChatById(localStorage.token, chatId, { |
|
messages: messages, |
|
history: history |
|
}); |
|
}; |
|
</script> |
|
|
|
<div class="h-full flex"> |
|
{#if 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} |
|
{#each messages as message, messageIdx} |
|
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}"> |
|
<div |
|
class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null |
|
? 'max-w-full' |
|
: 'max-w-5xl'} mx-auto rounded-lg group" |
|
> |
|
{#if message.role === 'user'} |
|
<UserMessage |
|
on:delete={() => deleteMessageHandler(message.id)} |
|
{user} |
|
{readOnly} |
|
{message} |
|
isFirstMessage={messageIdx === 0} |
|
siblings={message.parentId !== null |
|
? history.messages[message.parentId]?.childrenIds ?? [] |
|
: Object.values(history.messages) |
|
.filter((message) => message.parentId === null) |
|
.map((message) => message.id) ?? []} |
|
{confirmEditMessage} |
|
{showPreviousMessage} |
|
{showNextMessage} |
|
copyToClipboard={copyToClipboardWithToast} |
|
/> |
|
{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1} |
|
{#key message.id && history.currentId} |
|
<ResponseMessage |
|
{message} |
|
siblings={history.messages[message.parentId]?.childrenIds ?? []} |
|
isLastMessage={messageIdx + 1 === messages.length} |
|
{readOnly} |
|
{updateChatMessages} |
|
{confirmEditResponseMessage} |
|
{showPreviousMessage} |
|
{showNextMessage} |
|
{rateMessage} |
|
copyToClipboard={copyToClipboardWithToast} |
|
{continueGeneration} |
|
{regenerateResponse} |
|
on:action={async (e) => { |
|
await chatActionHandler(chatId, e.detail, message.model, message.id); |
|
}} |
|
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 |
|
}); |
|
}} |
|
/> |
|
{/key} |
|
{:else} |
|
{#key message.parentId} |
|
<CompareMessages |
|
bind:history |
|
{messages} |
|
{readOnly} |
|
{chatId} |
|
parentMessage={history.messages[message.parentId]} |
|
{messageIdx} |
|
{updateChatMessages} |
|
{confirmEditResponseMessage} |
|
{rateMessage} |
|
copyToClipboard={copyToClipboardWithToast} |
|
{continueGeneration} |
|
{regenerateResponse} |
|
on:change={async () => { |
|
await updateChatById(localStorage.token, chatId, { |
|
messages: messages, |
|
history: history |
|
}); |
|
|
|
if (autoScroll) { |
|
const element = document.getElementById('messages-container'); |
|
autoScroll = |
|
element.scrollHeight - element.scrollTop <= element.clientHeight + 50; |
|
setTimeout(() => { |
|
scrollToBottom(); |
|
}, 100); |
|
} |
|
}} |
|
/> |
|
{/key} |
|
{/if} |
|
</div> |
|
</div> |
|
{/each} |
|
|
|
{#if bottomPadding} |
|
<div class=" pb-6" /> |
|
{/if} |
|
{/key} |
|
</div> |
|
{/if} |
|
</div> |
|
|