Browse Source

feat: many model interaction ui

Timothy J. Baek 11 months ago
parent
commit
676a4dffd0

+ 34 - 5
src/lib/components/chat/Messages.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { v4 as uuidv4 } from 'uuid';
 
-	import { chats, config, modelfiles, settings, user as _user } from '$lib/stores';
+	import { chats, config, modelfiles, settings, user as _user, mobile } from '$lib/stores';
 	import { tick, getContext } from 'svelte';
 
 	import { toast } from 'svelte-sonner';
@@ -13,6 +13,8 @@
 	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');
 
@@ -28,10 +30,10 @@
 	export let processing = '';
 	export let bottomPadding = false;
 	export let autoScroll;
-	export let selectedModels;
 	export let history = {};
 	export let messages = [];
 
+	export let selectedModels;
 	export let selectedModelfiles = [];
 
 	$: if (autoScroll && bottomPadding) {
@@ -63,7 +65,8 @@
 			childrenIds: [],
 			role: 'user',
 			content: userPrompt,
-			...(history.messages[messageId].files && { files: history.messages[messageId].files })
+			...(history.messages[messageId].files && { files: history.messages[messageId].files }),
+			models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
 		};
 
 		let messageParentId = history.messages[messageId].parentId;
@@ -79,7 +82,7 @@
 		history.currentId = userMessageId;
 
 		await tick();
-		await sendPrompt(userPrompt, userMessageId, chatId);
+		await sendPrompt(userPrompt, userMessageId);
 	};
 
 	const updateChatMessages = async () => {
@@ -309,7 +312,7 @@
 									{showNextMessage}
 									copyToClipboard={copyToClipboardWithToast}
 								/>
-							{:else}
+							{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
 								{#key message.id}
 									<ResponseMessage
 										{message}
@@ -337,6 +340,32 @@
 										}}
 									/>
 								{/key}
+							{:else}
+								{#key message.parentId}
+									<CompareMessages
+										bind:history
+										{messages}
+										{chatId}
+										parentMessage={history.messages[message.parentId]}
+										{messageIdx}
+										{selectedModelfiles}
+										{updateChatMessages}
+										{confirmEditResponseMessage}
+										{rateMessage}
+										copyToClipboard={copyToClipboardWithToast}
+										{continueGeneration}
+										{regenerateResponse}
+										on:change={() => {
+											const element = document.getElementById('messages-container');
+											autoScroll =
+												element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
+
+											setTimeout(() => {
+												scrollToBottom();
+											}, 100);
+										}}
+									/>
+								{/key}
 							{/if}
 						</div>
 					</div>

+ 156 - 0
src/lib/components/chat/Messages/CompareMessages.svelte

@@ -0,0 +1,156 @@
+<script lang="ts">
+	import { createEventDispatcher } from 'svelte';
+
+	import { updateChatById } from '$lib/apis/chats';
+	import { onMount, tick } from 'svelte';
+	import ResponseMessage from './ResponseMessage.svelte';
+
+	export let chatId;
+
+	export let history;
+	export let messages = [];
+	export let messageIdx;
+
+	export let parentMessage;
+
+	export let selectedModelfiles;
+
+	export let updateChatMessages: Function;
+	export let confirmEditResponseMessage: Function;
+	export let rateMessage: Function;
+
+	export let copyToClipboard: Function;
+	export let continueGeneration: Function;
+	export let regenerateResponse: Function;
+
+	const dispatch = createEventDispatcher();
+
+	let currentMessageId;
+
+	let groupedMessagesIdx = {};
+	let groupedMessages = {};
+
+	$: groupedMessages = parentMessage?.models.reduce((a, model) => {
+		const modelMessages = parentMessage?.childrenIds
+			.map((id) => history.messages[id])
+			.filter((m) => m.model === model);
+
+		return {
+			...a,
+			[model]: { messages: modelMessages }
+		};
+	}, {});
+
+	onMount(async () => {
+		await tick();
+		currentMessageId = messages[messageIdx].id;
+
+		for (const model of parentMessage?.models) {
+			const idx = groupedMessages[model].messages.findIndex((m) => m.id === currentMessageId);
+
+			if (idx !== -1) {
+				groupedMessagesIdx[model] = idx;
+			} else {
+				groupedMessagesIdx[model] = 0;
+			}
+		}
+	});
+</script>
+
+<div>
+	<div class="flex snap-x snap-mandatory overflow-x-auto scrollbar-none">
+		{#each Object.keys(groupedMessages) as model}
+			{#if groupedMessagesIdx[model] !== undefined && groupedMessages[model].messages.length > 0}
+				<!-- svelte-ignore a11y-no-static-element-interactions -->
+				<!-- svelte-ignore a11y-click-events-have-key-events -->
+
+				<div
+					class=" snap-center min-w-96 w-full max-w-full m-1 outline outline-1 {history.messages[
+						currentMessageId
+					].model === model
+						? 'outline-gray-200 dark:outline-gray-700 outline-2'
+						: 'outline-gray-100 dark:outline-gray-850 '} transition p-6 rounded-3xl"
+					on:click={() => {
+						currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
+
+						let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
+
+						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;
+						dispatch('change');
+					}}
+				>
+					<ResponseMessage
+						message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
+						modelfiles={selectedModelfiles}
+						siblings={groupedMessages[model].messages.map((m) => m.id)}
+						isLastMessage={true}
+						{updateChatMessages}
+						{confirmEditResponseMessage}
+						showPreviousMessage={() => {
+							groupedMessagesIdx[model] = Math.max(0, groupedMessagesIdx[model] - 1);
+							let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
+
+							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;
+
+							dispatch('change');
+						}}
+						showNextMessage={() => {
+							groupedMessagesIdx[model] = Math.min(
+								groupedMessages[model].messages.length - 1,
+								groupedMessagesIdx[model] + 1
+							);
+
+							let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
+							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;
+
+							dispatch('change');
+						}}
+						{rateMessage}
+						{copyToClipboard}
+						{continueGeneration}
+						regenerateResponse={async (model) => {
+							regenerateResponse(model);
+							await tick();
+							groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
+						}}
+						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
+							});
+						}}
+					/>
+				</div>
+			{/if}
+		{/each}
+	</div>
+</div>

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

@@ -69,7 +69,7 @@
 
 	let selectedCitation = null;
 
-	$: tokens = marked.lexer(sanitizeResponseContent(message.content));
+	$: tokens = marked.lexer(sanitizeResponseContent(message?.content));
 
 	const renderer = new marked.Renderer();
 
@@ -499,7 +499,7 @@
 									class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
 								>
 									{#if siblings.length > 1}
-										<div class="flex self-center" dir="ltr">
+										<div class="flex self-center min-w-fit" dir="ltr">
 											<button
 												class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
 												on:click={() => {
@@ -523,7 +523,7 @@
 											</button>
 
 											<div
-												class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"
+												class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit"
 											>
 												{siblings.indexOf(message.id) + 1}/{siblings.length}
 											</div>
@@ -894,7 +894,9 @@
 													class="{isLastMessage
 														? 'visible'
 														: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
-													on:click={regenerateResponse}
+													on:click={() => {
+														regenerateResponse(message.model);
+													}}
 												>
 													<svg
 														xmlns="http://www.w3.org/2000/svg"

+ 41 - 38
src/routes/(app)/+page.svelte

@@ -202,6 +202,7 @@
 				user: _user ?? undefined,
 				content: userPrompt,
 				files: files.length > 0 ? files : undefined,
+				models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx),
 				timestamp: Math.floor(Date.now() / 1000) // Unix epoch
 			};
 
@@ -250,48 +251,50 @@
 		}
 	};
 
-	const sendPrompt = async (prompt, parentId) => {
+	const sendPrompt = async (prompt, parentId, modelId = null) => {
 		const _chatId = JSON.parse(JSON.stringify($chatId));
 
 		await Promise.all(
-			(atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(async (modelId) => {
-				console.log('modelId', modelId);
-				const model = $models.filter((m) => m.id === modelId).at(0);
-
-				if (model) {
-					// Create response message
-					let responseMessageId = uuidv4();
-					let responseMessage = {
-						parentId: parentId,
-						id: responseMessageId,
-						childrenIds: [],
-						role: 'assistant',
-						content: '',
-						model: model.id,
-						timestamp: Math.floor(Date.now() / 1000) // Unix epoch
-					};
-
-					// Add message to history and Set currentId to messageId
-					history.messages[responseMessageId] = responseMessage;
-					history.currentId = responseMessageId;
-
-					// Append messageId to childrenIds of parent message
-					if (parentId !== null) {
-						history.messages[parentId].childrenIds = [
-							...history.messages[parentId].childrenIds,
-							responseMessageId
-						];
-					}
+			(modelId ? [modelId] : atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(
+				async (modelId) => {
+					console.log('modelId', modelId);
+					const model = $models.filter((m) => m.id === modelId).at(0);
+
+					if (model) {
+						// Create response message
+						let responseMessageId = uuidv4();
+						let responseMessage = {
+							parentId: parentId,
+							id: responseMessageId,
+							childrenIds: [],
+							role: 'assistant',
+							content: '',
+							model: model.id,
+							timestamp: Math.floor(Date.now() / 1000) // Unix epoch
+						};
+
+						// Add message to history and Set currentId to messageId
+						history.messages[responseMessageId] = responseMessage;
+						history.currentId = responseMessageId;
+
+						// Append messageId to childrenIds of parent message
+						if (parentId !== null) {
+							history.messages[parentId].childrenIds = [
+								...history.messages[parentId].childrenIds,
+								responseMessageId
+							];
+						}
 
-					if (model?.external) {
-						await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
-					} else if (model) {
-						await sendPromptOllama(model, prompt, responseMessageId, _chatId);
+						if (model?.external) {
+							await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
+						} else if (model) {
+							await sendPromptOllama(model, prompt, responseMessageId, _chatId);
+						}
+					} else {
+						toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
 					}
-				} else {
-					toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
 				}
-			})
+			)
 		);
 
 		await chats.set(await getChatList(localStorage.token));
@@ -756,7 +759,7 @@
 		console.log('stopResponse');
 	};
 
-	const regenerateResponse = async () => {
+	const regenerateResponse = async (modelId) => {
 		console.log('regenerateResponse');
 		if (messages.length != 0 && messages.at(-1).done == true) {
 			messages.splice(messages.length - 1, 1);
@@ -765,7 +768,7 @@
 			let userMessage = messages.at(-1);
 			let userPrompt = userMessage.content;
 
-			await sendPrompt(userPrompt, userMessage.id);
+			await sendPrompt(userPrompt, userMessage.id, modelId);
 		}
 	};
 

+ 43 - 38
src/routes/(app)/c/[id]/+page.svelte

@@ -210,7 +210,8 @@
 				user: _user ?? undefined,
 				content: userPrompt,
 				files: files.length > 0 ? files : undefined,
-				timestamp: Math.floor(Date.now() / 1000) // Unix epoch
+				timestamp: Math.floor(Date.now() / 1000), // Unix epoch
+				models: selectedModels
 			};
 
 			// Add message to history and Set currentId to messageId
@@ -255,47 +256,51 @@
 			await sendPrompt(userPrompt, userMessageId);
 		}
 	};
-	const sendPrompt = async (prompt, parentId) => {
+
+	const sendPrompt = async (prompt, parentId, modelId = null) => {
 		const _chatId = JSON.parse(JSON.stringify($chatId));
 
 		await Promise.all(
-			(atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(async (modelId) => {
-				const model = $models.filter((m) => m.id === modelId).at(0);
-
-				if (model) {
-					// Create response message
-					let responseMessageId = uuidv4();
-					let responseMessage = {
-						parentId: parentId,
-						id: responseMessageId,
-						childrenIds: [],
-						role: 'assistant',
-						content: '',
-						model: model.id,
-						timestamp: Math.floor(Date.now() / 1000) // Unix epoch
-					};
-
-					// Add message to history and Set currentId to messageId
-					history.messages[responseMessageId] = responseMessage;
-					history.currentId = responseMessageId;
-
-					// Append messageId to childrenIds of parent message
-					if (parentId !== null) {
-						history.messages[parentId].childrenIds = [
-							...history.messages[parentId].childrenIds,
-							responseMessageId
-						];
-					}
+			(modelId ? [modelId] : atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(
+				async (modelId) => {
+					console.log('modelId', modelId);
+					const model = $models.filter((m) => m.id === modelId).at(0);
+
+					if (model) {
+						// Create response message
+						let responseMessageId = uuidv4();
+						let responseMessage = {
+							parentId: parentId,
+							id: responseMessageId,
+							childrenIds: [],
+							role: 'assistant',
+							content: '',
+							model: model.id,
+							timestamp: Math.floor(Date.now() / 1000) // Unix epoch
+						};
+
+						// Add message to history and Set currentId to messageId
+						history.messages[responseMessageId] = responseMessage;
+						history.currentId = responseMessageId;
+
+						// Append messageId to childrenIds of parent message
+						if (parentId !== null) {
+							history.messages[parentId].childrenIds = [
+								...history.messages[parentId].childrenIds,
+								responseMessageId
+							];
+						}
 
-					if (model?.external) {
-						await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
-					} else if (model) {
-						await sendPromptOllama(model, prompt, responseMessageId, _chatId);
+						if (model?.external) {
+							await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
+						} else if (model) {
+							await sendPromptOllama(model, prompt, responseMessageId, _chatId);
+						}
+					} else {
+						toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
 					}
-				} else {
-					toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
 				}
-			})
+			)
 		);
 
 		await chats.set(await getChatList(localStorage.token));
@@ -759,7 +764,7 @@
 		console.log('stopResponse');
 	};
 
-	const regenerateResponse = async () => {
+	const regenerateResponse = async (modelId = null) => {
 		console.log('regenerateResponse');
 		if (messages.length != 0 && messages.at(-1).done == true) {
 			messages.splice(messages.length - 1, 1);
@@ -768,7 +773,7 @@
 			let userMessage = messages.at(-1);
 			let userPrompt = userMessage.content;
 
-			await sendPrompt(userPrompt, userMessage.id);
+			await sendPrompt(userPrompt, userMessage.id, modelId);
 		}
 	};