ai / frontend /src /lib /components /chat /Messages /MultiResponseMessages.svelte
fdrah
initial-commit
9e93b10
Raw
History Blame Contribute Delete
14.6 kB
<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 { REXPRO_BASE_URL } from "$lib/constants";
import equal from "fast-deep-equal";
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 (!equal(message, 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}