Same optimization as ResponseMessage: add O(1) fast-path check on content and done fields before falling back to full JSON.stringify comparison. Avoids expensive serialization when only content changes during streaming.
448 lines
14 KiB
Svelte
448 lines
14 KiB
Svelte
<script lang="ts">
|
|
import dayjs from 'dayjs';
|
|
import { onMount, tick, getContext } from 'svelte';
|
|
import { createEventDispatcher } from 'svelte';
|
|
|
|
import { mobile, models, settings } from '$lib/stores';
|
|
|
|
import { generateMoACompletion } from '$lib/apis';
|
|
import { updateChatById } from '$lib/apis/chats';
|
|
import { createOpenAITextStream } from '$lib/apis/streaming';
|
|
|
|
import ResponseMessage from './ResponseMessage.svelte';
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
import Merge from '$lib/components/icons/Merge.svelte';
|
|
|
|
import Markdown from './Markdown.svelte';
|
|
import Name from './Name.svelte';
|
|
import Skeleton from './Skeleton.svelte';
|
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
|
import ProfileImage from './ProfileImage.svelte';
|
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
|
const i18n = getContext('i18n');
|
|
dayjs.extend(localizedFormat);
|
|
|
|
export let chatId;
|
|
export let history;
|
|
export let messageId;
|
|
export let selectedModels = [];
|
|
|
|
export let isLastMessage;
|
|
export let readOnly = false;
|
|
export let editCodeBlock = true;
|
|
|
|
export let setInputText: Function = () => {};
|
|
export let updateChat: Function;
|
|
export let editMessage: Function;
|
|
export let saveMessage: Function;
|
|
export let rateMessage: Function;
|
|
export let actionMessage: Function;
|
|
|
|
export let submitMessage: Function;
|
|
export let deleteMessage: Function;
|
|
|
|
export let continueResponse: Function;
|
|
export let regenerateResponse: Function;
|
|
export let mergeResponses: Function;
|
|
|
|
export let addMessages: Function;
|
|
|
|
export let triggerScroll: Function;
|
|
|
|
export let topPadding = false;
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
let currentMessageId;
|
|
let parentMessage;
|
|
let groupedMessageIds = {};
|
|
let groupedMessageIdsIdx = {};
|
|
|
|
let selectedModelIdx = null;
|
|
|
|
let message = structuredClone(history.messages[messageId]);
|
|
$: if (history.messages) {
|
|
const source = history.messages[messageId];
|
|
if (source) {
|
|
if (message.content !== source.content || message.done !== source.done) {
|
|
message = structuredClone(source);
|
|
} else if (JSON.stringify(message) !== JSON.stringify(source)) {
|
|
message = structuredClone(source);
|
|
}
|
|
}
|
|
}
|
|
|
|
const gotoMessage = async (modelIdx, messageIdx) => {
|
|
// Clamp messageIdx to ensure it's within valid range
|
|
groupedMessageIdsIdx[modelIdx] = Math.max(
|
|
0,
|
|
Math.min(messageIdx, groupedMessageIds[modelIdx].messageIds.length - 1)
|
|
);
|
|
|
|
// Get the messageId at the specified index
|
|
let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
|
|
console.log(messageId);
|
|
|
|
// Traverse the branch to find the deepest child message
|
|
let messageChildrenIds = history.messages[messageId].childrenIds;
|
|
while (messageChildrenIds.length !== 0) {
|
|
messageId = messageChildrenIds.at(-1);
|
|
messageChildrenIds = history.messages[messageId].childrenIds;
|
|
}
|
|
|
|
// Update the current message ID in history
|
|
history.currentId = messageId;
|
|
|
|
// Await UI updates
|
|
await tick();
|
|
await updateChat();
|
|
|
|
// Trigger scrolling after navigation
|
|
triggerScroll();
|
|
};
|
|
|
|
const showPreviousMessage = async (modelIdx) => {
|
|
groupedMessageIdsIdx[modelIdx] = Math.max(0, groupedMessageIdsIdx[modelIdx] - 1);
|
|
|
|
let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
|
|
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;
|
|
|
|
await tick();
|
|
await updateChat();
|
|
triggerScroll();
|
|
};
|
|
|
|
const showNextMessage = async (modelIdx) => {
|
|
groupedMessageIdsIdx[modelIdx] = Math.min(
|
|
groupedMessageIds[modelIdx].messageIds.length - 1,
|
|
groupedMessageIdsIdx[modelIdx] + 1
|
|
);
|
|
|
|
let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
|
|
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;
|
|
|
|
await tick();
|
|
await updateChat();
|
|
triggerScroll();
|
|
};
|
|
|
|
const initHandler = async () => {
|
|
console.log('multiresponse:initHandler');
|
|
await tick();
|
|
|
|
currentMessageId = messageId;
|
|
parentMessage = history.messages[messageId].parentId
|
|
? history.messages[history.messages[messageId].parentId]
|
|
: null;
|
|
|
|
groupedMessageIds = parentMessage?.models.reduce((a, model, modelIdx) => {
|
|
// Find all messages that are children of the parent message and have the same model
|
|
let modelMessageIds = parentMessage?.childrenIds
|
|
.map((id) => history.messages[id])
|
|
.filter((m) => m?.modelIdx === modelIdx)
|
|
.map((m) => m.id);
|
|
|
|
// Legacy support for messages that don't have a modelIdx
|
|
// Find all messages that are children of the parent message and have the same model
|
|
if (modelMessageIds.length === 0) {
|
|
let modelMessages = parentMessage?.childrenIds
|
|
.map((id) => history.messages[id])
|
|
.filter((m) => m?.model === model);
|
|
|
|
modelMessages.forEach((m) => {
|
|
m.modelIdx = modelIdx;
|
|
});
|
|
|
|
modelMessageIds = modelMessages.map((m) => m.id);
|
|
}
|
|
|
|
return {
|
|
...a,
|
|
[modelIdx]: { messageIds: modelMessageIds }
|
|
};
|
|
}, {});
|
|
|
|
groupedMessageIdsIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
|
|
const idx = groupedMessageIds[modelIdx].messageIds.findIndex((id) => id === messageId);
|
|
if (idx !== -1) {
|
|
return {
|
|
...a,
|
|
[modelIdx]: idx
|
|
};
|
|
} else {
|
|
return {
|
|
...a,
|
|
[modelIdx]: groupedMessageIds[modelIdx].messageIds.length - 1
|
|
};
|
|
}
|
|
}, {});
|
|
|
|
selectedModelIdx = history.messages[messageId]?.modelIdx;
|
|
|
|
console.log(groupedMessageIds, groupedMessageIdsIdx);
|
|
|
|
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 responses = Object.keys(groupedMessageIds).map((modelIdx) => {
|
|
const { messageIds } = groupedMessageIds[modelIdx];
|
|
const messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
|
|
|
|
return history.messages[messageId].content;
|
|
});
|
|
mergeResponses(messageId, responses, chatId);
|
|
};
|
|
|
|
onMount(async () => {
|
|
await initHandler();
|
|
await tick();
|
|
|
|
if ($settings?.scrollOnBranchChange ?? true) {
|
|
const messageElement = document.getElementById(`message-${messageId}`);
|
|
if (messageElement) {
|
|
messageElement.scrollIntoView({ block: 'start' });
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{#if parentMessage}
|
|
<div>
|
|
<div
|
|
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
|
id="responses-container-{chatId}-{parentMessage.id}"
|
|
>
|
|
{#if $settings?.displayMultiModelResponsesInTabs ?? false}
|
|
<div class="w-full">
|
|
<div class=" flex w-full mb-4.5 border-b border-gray-200 dark:border-gray-850">
|
|
<div
|
|
class="flex gap-2 scrollbar-none overflow-x-auto w-fit text-center font-medium bg-transparent pt-1 text-sm"
|
|
on:wheel|preventDefault={(e) => {
|
|
e.currentTarget.scrollLeft += e.deltaY;
|
|
}}
|
|
>
|
|
{#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]]}
|
|
|
|
{@const model = $models.find((m) => m.id === history.messages[_messageId]?.model)}
|
|
|
|
<button
|
|
class="min-w-fit {selectedModelIdx == modelIdx
|
|
? ' dark:border-gray-300 '
|
|
: ' opacity-35 border-transparent'} pb-1.5 px-2.5 transition border-b-2"
|
|
on:click={async () => {
|
|
if (selectedModelIdx != modelIdx) {
|
|
selectedModelIdx = modelIdx;
|
|
}
|
|
|
|
onGroupClick(_messageId, modelIdx);
|
|
}}
|
|
>
|
|
<div class="flex items-center gap-1.5">
|
|
<div class="-translate-y-[1px]">
|
|
{model ? `${model.name}` : history.messages[_messageId]?.model}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
{#if selectedModelIdx !== null}
|
|
{#key history.currentId}
|
|
{#if message}
|
|
<ResponseMessage
|
|
{chatId}
|
|
{history}
|
|
messageId={message?.id}
|
|
{selectedModels}
|
|
isLastMessage={true}
|
|
siblings={groupedMessageIds[selectedModelIdx].messageIds}
|
|
gotoMessage={(message, messageIdx) => gotoMessage(selectedModelIdx, messageIdx)}
|
|
showPreviousMessage={() => showPreviousMessage(selectedModelIdx)}
|
|
showNextMessage={() => showNextMessage(selectedModelIdx)}
|
|
{setInputText}
|
|
{updateChat}
|
|
{editMessage}
|
|
{saveMessage}
|
|
{rateMessage}
|
|
{deleteMessage}
|
|
{actionMessage}
|
|
{submitMessage}
|
|
{continueResponse}
|
|
regenerateResponse={async (message, prompt = null) => {
|
|
regenerateResponse(message, prompt);
|
|
await tick();
|
|
groupedMessageIdsIdx[selectedModelIdx] =
|
|
groupedMessageIds[selectedModelIdx].messageIds.length - 1;
|
|
}}
|
|
{addMessages}
|
|
{readOnly}
|
|
{topPadding}
|
|
/>
|
|
{/if}
|
|
{/key}
|
|
{/if}
|
|
</div>
|
|
{: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/30 dark:border-gray-850/30 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, prompt = null) => {
|
|
regenerateResponse(message, prompt);
|
|
await tick();
|
|
groupedMessageIdsIdx[modelIdx] =
|
|
groupedMessageIds[modelIdx].messageIds.length - 1;
|
|
}}
|
|
{addMessages}
|
|
{readOnly}
|
|
{editCodeBlock}
|
|
{topPadding}
|
|
/>
|
|
{/if}
|
|
{/key}
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
|
|
{#if !readOnly}
|
|
{#if !Object.keys(groupedMessageIds).find((modelIdx) => {
|
|
const { messageIds } = groupedMessageIds[modelIdx];
|
|
const _messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
|
|
return !history.messages[_messageId]?.done ?? false;
|
|
})}
|
|
<div class="flex justify-end">
|
|
<div class="w-full">
|
|
{#if history.messages[messageId]?.merged?.status}
|
|
{@const message = history.messages[messageId]?.merged}
|
|
|
|
<div class="w-full rounded-xl pl-5 pr-2 py-2 mt-2">
|
|
<Name>
|
|
{$i18n.t('Merged Response')}
|
|
|
|
{#if message.timestamp}
|
|
<span
|
|
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
|
|
>
|
|
{dayjs(message.timestamp * 1000).format('LT')}
|
|
</span>
|
|
{/if}
|
|
</Name>
|
|
|
|
<div class="mt-1 markdown-prose w-full min-w-full">
|
|
{#if (message?.content ?? '') === ''}
|
|
<Skeleton />
|
|
{:else}
|
|
<Markdown id={`merged`} content={message.content ?? ''} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if isLastMessage}
|
|
<div class=" shrink-0 text-gray-600 dark:text-gray-500 mt-1">
|
|
<Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
|
|
<button
|
|
type="button"
|
|
id="merge-response-button"
|
|
class="{true
|
|
? 'visible'
|
|
: 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
|
|
on:click={() => {
|
|
mergeResponsesHandler();
|
|
}}
|
|
>
|
|
<Merge className=" size-5 " />
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{/if}
|