Timothy J. Baek 7 mesiacov pred
rodič
commit
84c1810b6e

+ 15 - 1
src/lib/components/chat/Messages.svelte

@@ -333,7 +333,7 @@
 					{#each messages as message, messageIdx (message.id)}
 						<Message
 							{chatId}
-							{history}
+							bind:history
 							messageId={message.id}
 							idx={messageIdx}
 							{user}
@@ -344,6 +344,20 @@
 							{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>

+ 44 - 40
src/lib/components/chat/Messages/Message.svelte

@@ -1,7 +1,8 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 
-	import { tick, getContext, onMount } from 'svelte';
+	import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
+	const dispatch = createEventDispatcher();
 	const i18n = getContext('i18n');
 
 	import { settings } from '$lib/stores';
@@ -20,7 +21,7 @@
 
 	export let user;
 
-	export let scrollToBottom;
+	export let updateChatHistory;
 	export let chatActionHandler;
 
 	export let showPreviousMessage;
@@ -36,22 +37,9 @@
 	// MultiResponseMessages
 	export let mergeResponses;
 
+	export let autoScroll = false;
 	export let readOnly = false;
 
-	let message = JSON.parse(JSON.stringify(history.messages[messageId]));
-	$: if (history.messages) {
-		if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
-			message = JSON.parse(JSON.stringify(history.messages[messageId]));
-		}
-	}
-
-	const copyToClipboardWithToast = async (text) => {
-		const res = await copyToClipboard(text);
-		if (res) {
-			toast.success($i18n.t('Copying to clipboard was successful!'));
-		}
-	};
-
 	onMount(() => {
 		console.log('message', idx);
 	});
@@ -62,28 +50,30 @@
 		? 'max-w-full'
 		: 'max-w-5xl'} mx-auto rounded-lg group"
 >
-	{#if message}
-		{#if message.role === 'user'}
+	{#if history.messages[messageId]}
+		{#if history.messages[messageId].role === 'user'}
 			<UserMessage
 				{user}
-				{message}
+				{history}
+				{messageId}
 				isFirstMessage={idx === 0}
-				siblings={message.parentId !== null
-					? (history.messages[message.parentId]?.childrenIds ?? [])
+				siblings={history.messages[messageId].parentId !== null
+					? (history.messages[history.messages[messageId].parentId]?.childrenIds ?? [])
 					: (Object.values(history.messages)
 							.filter((message) => message.parentId === null)
 							.map((message) => message.id) ?? [])}
 				{showPreviousMessage}
 				{showNextMessage}
 				{editMessage}
-				on:delete={() => deleteMessage(message.id)}
+				on:delete={() => deleteMessage(messageId)}
 				{readOnly}
 			/>
-		{:else if (history.messages[message.parentId]?.models?.length ?? 1) === 1}
+		{:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1}
 			<ResponseMessage
-				{message}
+				{history}
+				{messageId}
 				isLastMessage={messageId === history.currentId}
-				siblings={history.messages[message.parentId]?.childrenIds ?? []}
+				siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
 				{showPreviousMessage}
 				{showNextMessage}
 				{editMessage}
@@ -92,6 +82,7 @@
 				{regenerateResponse}
 				on:action={async (e) => {
 					console.log('action', e);
+					const message = history.messages[messageId];
 					if (typeof e.detail === 'string') {
 						await chatActionHandler(chatId, e.detail, message.model, message.id);
 					} else {
@@ -101,7 +92,7 @@
 				}}
 				on:update={async (e) => {
 					console.log('update', e);
-					// call updateChatHistory
+					updateChatHistory();
 				}}
 				on:save={async (e) => {
 					console.log('save', e);
@@ -124,18 +115,16 @@
 			<MultiResponseMessages
 				bind:history
 				{chatId}
-				messageIds={[]}
-				parentMessage={history.messages[message.parentId]}
-				isLastMessage={messageId === history.currentId}
-				{editMessage}
+				{messageId}
+				isLastMessage={messageId === history?.currentId}
 				{rateMessage}
+				{editMessage}
 				{continueResponse}
-				{mergeResponses}
 				{regenerateResponse}
-				copyToClipboard={copyToClipboardWithToast}
-				{readOnly}
+				{mergeResponses}
 				on:action={async (e) => {
 					console.log('action', e);
+					const message = history.messages[messageId];
 					if (typeof e.detail === 'string') {
 						await chatActionHandler(chatId, e.detail, message.model, message.id);
 					} else {
@@ -143,19 +132,34 @@
 						await chatActionHandler(chatId, id, message.model, message.id, event);
 					}
 				}}
+				on:update={async (e) => {
+					console.log('update', e);
+					updateChatHistory();
+				}}
+				on:save={async (e) => {
+					console.log('save', e);
+
+					const message = e.detail;
+					if (message) {
+						history.messages[message.id] = message;
+						await updateChatById(localStorage.token, chatId, {
+							history: history
+						});
+					} else {
+						await updateChatById(localStorage.token, chatId, {
+							history: history
+						});
+					}
+				}}
 				on:change={async () => {
+					await tick();
 					await updateChatById(localStorage.token, chatId, {
 						history: history
 					});
 
-					if (autoScroll) {
-						const element = document.getElementById('messages-container');
-						autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
-						setTimeout(() => {
-							scrollToBottom();
-						}, 100);
-					}
+					dispatch('scroll');
 				}}
+				{readOnly}
 			/>
 		{/if}
 	{/if}

+ 164 - 159
src/lib/components/chat/Messages/MultiResponseMessages.svelte

@@ -20,40 +20,36 @@
 	const i18n = getContext('i18n');
 
 	export let chatId;
-
 	export let history;
-	export let messageIdx;
+	export let messageId;
 
-	export let parentMessage;
 	export let isLastMessage;
-
 	export let readOnly = false;
 
-	export let updateChatMessages: Function;
-	export let confirmEditResponseMessage: Function;
+	export let editMessage: Function;
 	export let rateMessage: Function;
 
-	export let copyToClipboard: Function;
 	export let continueResponse: Function;
-	export let mergeResponses: Function;
 	export let regenerateResponse: Function;
-	export let saveNewResponseMessage: Function;
+	export let mergeResponses: Function;
 
 	const dispatch = createEventDispatcher();
 
 	let currentMessageId;
-	let groupedMessages = {};
-	let groupedMessagesIdx = {};
+	let parentMessage;
+	let groupedMessageIds = {};
+	let groupedMessageIdsIdx = {};
 
+	let message = JSON.parse(JSON.stringify(history.messages[messageId]));
 	$: if (history.messages) {
-		console.log('history.messages', history.messages);
-
-		initHandler();
+		if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
+			message = JSON.parse(JSON.stringify(history.messages[messageId]));
+		}
 	}
 
 	const showPreviousMessage = (modelIdx) => {
-		groupedMessagesIdx[modelIdx] = Math.max(0, groupedMessagesIdx[modelIdx] - 1);
-		let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
+		groupedMessageIdsIdx[modelIdx] = Math.max(0, groupedMessageIdsIdx[modelIdx] - 1);
+		let messageId = groupedMessageIds[modelIdx].messages[groupedMessageIdsIdx[modelIdx]].id;
 
 		console.log(messageId);
 		let messageChildrenIds = history.messages[messageId].childrenIds;
@@ -68,12 +64,12 @@
 	};
 
 	const showNextMessage = (modelIdx) => {
-		groupedMessagesIdx[modelIdx] = Math.min(
-			groupedMessages[modelIdx].messages.length - 1,
-			groupedMessagesIdx[modelIdx] + 1
+		groupedMessageIdsIdx[modelIdx] = Math.min(
+			groupedMessageIds[modelIdx].messages.length - 1,
+			groupedMessageIdsIdx[modelIdx] + 1
 		);
 
-		let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
+		let messageId = groupedMessageIds[modelIdx].messages[groupedMessageIdsIdx[modelIdx]].id;
 		console.log(messageId);
 
 		let messageChildrenIds = history.messages[messageId].childrenIds;
@@ -90,32 +86,41 @@
 	const initHandler = async () => {
 		console.log('multiresponse:initHandler');
 		await tick();
-		currentMessageId = messages[messageIdx].id;
 
-		groupedMessages = parentMessage?.models.reduce((a, model, modelIdx) => {
+		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 modelMessages = parentMessage?.childrenIds
+			let modelMessageIds = parentMessage?.childrenIds
 				.map((id) => history.messages[id])
-				.filter((m) => m?.modelIdx === modelIdx);
+				.filter((m) => m?.modelIdx === modelIdx)
+				.map((m) => m.id);
 
-			if (modelMessages.length === 0) {
-				modelMessages = parentMessage?.childrenIds
+			// 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]: { messages: modelMessages }
+				[modelIdx]: { messageIds: modelMessageIds }
 			};
 		}, {});
 
-		groupedMessagesIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
-			const idx = groupedMessages[modelIdx].messages.findIndex((m) => m.id === currentMessageId);
+		groupedMessageIdsIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
+			const idx = groupedMessageIds[modelIdx].messageIds.findIndex((id) => id === messageId);
 			if (idx !== -1) {
 				return {
 					...a,
@@ -128,14 +133,19 @@
 				};
 			}
 		}, {});
+
+		console.log(groupedMessageIds, groupedMessageIdsIdx);
+
+		await tick();
 	};
 
 	const mergeResponsesHandler = async () => {
-		const responses = Object.keys(groupedMessages).map((modelIdx) => {
-			const { messages } = groupedMessages[modelIdx];
-			return messages[groupedMessagesIdx[modelIdx]].content;
+		const responses = Object.keys(groupedMessageIds).map((modelIdx) => {
+			const { messageIds } = groupedMessageIds[modelIdx];
+
+			return messages[groupedMessageIdsIdx[modelIdx]].content;
 		});
-		mergeResponses(currentMessageId, responses, chatId);
+		mergeResponses(messageId, responses, chatId);
 	};
 
 	onMount(async () => {
@@ -143,134 +153,129 @@
 	});
 </script>
 
-<div>
-	<div
-		class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
-		id="responses-container-{chatId}-{parentMessage.id}"
-	>
-		{#each Object.keys(groupedMessages) as modelIdx}
-			{#if groupedMessagesIdx[modelIdx] !== undefined && groupedMessages[modelIdx].messages.length > 0}
-				<!-- svelte-ignore a11y-no-static-element-interactions -->
-				<!-- svelte-ignore a11y-click-events-have-key-events -->
-				{@const message = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]]}
-
-				<div
-					class=" snap-center w-full max-w-full m-1 border {history.messages[currentMessageId]
-						?.modelIdx == modelIdx
-						? `border-gray-100 dark:border-gray-800 border-[1.5px] ${
-								$mobile ? 'min-w-full' : 'min-w-[32rem]'
-							}`
-						: `border-gray-50 dark:border-gray-850 border-dashed ${
-								$mobile ? 'min-w-full' : 'min-w-80'
-							}`} transition-all p-5 rounded-2xl"
-					on:click={() => {
-						if (currentMessageId != message.id) {
-							currentMessageId = message.id;
-							let messageId = message.id;
-							console.log(messageId);
-							//
-							let messageChildrenIds = history.messages[messageId].childrenIds;
-							while (messageChildrenIds.length !== 0) {
-								messageId = messageChildrenIds.at(-1);
-								messageChildrenIds = history.messages[messageId].childrenIds;
+{#if parentMessage}
+	<div>
+		<div
+			class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
+			id="responses-container-{chatId}-{parentMessage.id}"
+		>
+			{#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
+							? `border-gray-100 dark:border-gray-800 border-[1.5px] ${
+									$mobile ? 'min-w-full' : 'min-w-[32rem]'
+								}`
+							: `border-gray-50 dark:border-gray-850 border-dashed ${
+									$mobile ? 'min-w-full' : 'min-w-80'
+								}`} transition-all p-5 rounded-2xl"
+						on:click={() => {
+							if (messageId != _messageId) {
+								let messageChildrenIds = history.messages[_messageId].childrenIds;
+								while (messageChildrenIds.length !== 0) {
+									messageId = messageChildrenIds.at(-1);
+									messageChildrenIds = history.messages[_messageId].childrenIds;
+								}
+								history.currentId = _messageId;
+								dispatch('change');
 							}
-							history.currentId = messageId;
-							dispatch('change');
-						}
-					}}
-				>
-					{#key history.currentId}
-						{#if message}
-							<ResponseMessage
-								{message}
-								siblings={groupedMessages[modelIdx].messages.map((m) => m.id)}
-								isLastMessage={true}
-								{updateChatMessages}
-								{saveNewResponseMessage}
-								{confirmEditResponseMessage}
-								showPreviousMessage={() => showPreviousMessage(modelIdx)}
-								showNextMessage={() => showNextMessage(modelIdx)}
-								{readOnly}
-								{rateMessage}
-								{copyToClipboard}
-								{continueResponse}
-								regenerateResponse={async (message) => {
-									regenerateResponse(message);
-									await tick();
-									groupedMessagesIdx[modelIdx] = groupedMessages[modelIdx].messages.length - 1;
-								}}
-								on:action={async (e) => {
-									dispatch('action', e.detail);
-								}}
-								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 history.currentId}
+							{#if message}
+								<ResponseMessage
+									{history}
+									messageId={_messageId}
+									isLastMessage={true}
+									siblings={groupedMessageIds[modelIdx].messageIds}
+									showPreviousMessage={() => showPreviousMessage(modelIdx)}
+									showNextMessage={() => showNextMessage(modelIdx)}
+									{rateMessage}
+									{editMessage}
+									{continueResponse}
+									regenerateResponse={async (message) => {
+										regenerateResponse(message);
+										await tick();
+										groupedMessageIdsIdx[modelIdx] =
+											groupedMessageIds[modelIdx].messageIds.length - 1;
+									}}
+									on:action={async (e) => {
+										dispatch('action', e.detail);
+									}}
+									on:update={async (e) => {
+										dispatch('update', e.detail);
+									}}
+									on:save={async (e) => {
+										dispatch('save', e.detail);
+									}}
+									{readOnly}
+								/>
+							{/if}
+						{/key}
+					</div>
+				{/if}
+			{/each}
+		</div>
+
+		{#if !readOnly && isLastMessage}
+			{#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">
+								<Name>
+									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($i18n.t('h:mm a'))}
+										</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}
-					{/key}
+					</div>
+
+					<div class=" flex-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 regenerate-response-button"
+								on:click={() => {
+									mergeResponsesHandler();
+								}}
+							>
+								<Merge className=" size-5 " />
+							</button>
+						</Tooltip>
+					</div>
 				</div>
 			{/if}
-		{/each}
-	</div>
-
-	{#if !readOnly && isLastMessage}
-		{#if !Object.keys(groupedMessages).find((modelIdx) => {
-			const { messages } = groupedMessages[modelIdx];
-			return !messages[groupedMessagesIdx[modelIdx]]?.done ?? false;
-		})}
-			<div class="flex justify-end">
-				<div class="w-full">
-					{#if history.messages[currentMessageId]?.merged?.status}
-						{@const message = history.messages[currentMessageId]?.merged}
-
-						<div class="w-full rounded-xl pl-5 pr-2 py-2">
-							<Name>
-								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($i18n.t('h:mm a'))}
-									</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>
-
-				<div class=" flex-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 regenerate-response-button"
-							on:click={() => {
-								mergeResponsesHandler();
-							}}
-						>
-							<Merge className=" size-5 " />
-						</button>
-					</Tooltip>
-				</div>
-			</div>
 		{/if}
-	{/if}
-</div>
+	</div>
+{/if}

+ 12 - 6
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -77,14 +77,17 @@
 		annotation?: { type: string; rating: number };
 	}
 
-	export let message: MessageType;
-	export let siblings;
-
-	export let isLastMessage = true;
+	export let history;
+	export let messageId;
 
-	export let readOnly = false;
+	let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId]));
+	$: if (history.messages) {
+		if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
+			message = JSON.parse(JSON.stringify(history.messages[messageId]));
+		}
+	}
 
-	export let saveNewResponseMessage: Function = () => {};
+	export let siblings;
 
 	export let showPreviousMessage: Function;
 	export let showNextMessage: Function;
@@ -95,6 +98,9 @@
 	export let continueResponse: Function;
 	export let regenerateResponse: Function;
 
+	export let isLastMessage = true;
+	export let readOnly = false;
+
 	let model = null;
 	$: model = $models.find((m) => m.id === message.model);
 

+ 10 - 5
src/lib/components/chat/Messages/UserMessage.svelte

@@ -20,25 +20,30 @@
 	const i18n = getContext('i18n');
 
 	const dispatch = createEventDispatcher();
-
 	export let user;
-	export let message;
+
+	export let history;
+	export let messageId;
+
 	export let siblings;
-	export let isFirstMessage: boolean;
 
 	export let showPreviousMessage: Function;
 	export let showNextMessage: Function;
 
 	export let editMessage: Function;
 
+	export let isFirstMessage: boolean;
 	export let readOnly: boolean;
 
 	let edit = false;
 	let editedContent = '';
 	let messageEditTextAreaElement: HTMLTextAreaElement;
 
-	$: if (message) {
-		console.log('message', message);
+	let message = JSON.parse(JSON.stringify(history.messages[messageId]));
+	$: if (history.messages) {
+		if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
+			message = JSON.parse(JSON.stringify(history.messages[messageId]));
+		}
 	}
 
 	const copyToClipboard = async (text) => {