瀏覽代碼

Merge pull request #5640 from open-webui/messages-render-optimisation

refac: messages render optimisation
Timothy Jaeryang Baek 7 月之前
父節點
當前提交
35f64cc53f

+ 128 - 137
src/lib/components/chat/Chat.svelte

@@ -5,6 +5,8 @@
 	import { PaneGroup, Pane, PaneResizer } from 'paneforge';
 
 	import { getContext, onDestroy, onMount, tick } from 'svelte';
+	const i18n: Writable<i18nType> = getContext('i18n');
+
 	import { goto } from '$app/navigation';
 	import { page } from '$app/stores';
 
@@ -67,11 +69,9 @@
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import ChatControls from './ChatControls.svelte';
 	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
-	import EllipsisVertical from '../icons/EllipsisVertical.svelte';
-
-	const i18n: Writable<i18nType> = getContext('i18n');
 
 	export let chatIdProp = '';
+
 	let loaded = false;
 	const eventTarget = new EventTarget();
 	let controlPane;
@@ -89,9 +89,10 @@
 	let eventConfirmationInputValue = '';
 	let eventCallback = null;
 
+	let chatIdUnsubscriber: Unsubscriber | undefined;
+
 	let selectedModels = [''];
 	let atSelectedModel: Model | undefined;
-
 	let selectedModelIds = [];
 	$: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
 
@@ -102,35 +103,17 @@
 	let tags = [];
 
 	let title = '';
-	let prompt = '';
-
-	let chatFiles = [];
-	let files = [];
-	let messages = [];
 	let history = {
 		messages: {},
 		currentId: null
 	};
 
+	// Chat Input
+	let prompt = '';
+	let chatFiles = [];
+	let files = [];
 	let params = {};
 
-	let chatIdUnsubscriber: Unsubscriber | undefined;
-
-	$: if (history.currentId !== null) {
-		let _messages = [];
-		let currentMessage = history.messages[history.currentId];
-		while (currentMessage) {
-			_messages.unshift({ ...currentMessage });
-			currentMessage =
-				currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null;
-		}
-
-		// This is most likely causing the performance issue
-		messages = _messages;
-	} else {
-		messages = [];
-	}
-
 	$: if (chatIdProp) {
 		(async () => {
 			console.log(chatIdProp);
@@ -227,8 +210,6 @@
 			} else {
 				console.log('Unknown message type', data);
 			}
-
-			messages = messages;
 		}
 	};
 
@@ -310,6 +291,9 @@
 				showOverview.set(false);
 			}
 		});
+
+		const chatInput = document.getElementById('chat-textarea');
+		chatInput?.focus();
 	});
 
 	onDestroy(() => {
@@ -331,7 +315,6 @@
 		autoScroll = true;
 
 		title = '';
-		messages = [];
 		history = {
 			messages: {},
 			currentId: null
@@ -428,8 +411,8 @@
 				autoScroll = true;
 				await tick();
 
-				if (messages.length > 0) {
-					history.messages[messages.at(-1).id].done = true;
+				if (history.currentId) {
+					history.messages[history.currentId].done = true;
 				}
 				await tick();
 
@@ -448,8 +431,12 @@
 	};
 
 	const createMessagesList = (responseMessageId) => {
+		if (responseMessageId === null) {
+			return [];
+		}
+
 		const message = history.messages[responseMessageId];
-		if (message.parentId) {
+		if (message?.parentId) {
 			return [...createMessagesList(message.parentId), message];
 		} else {
 			return [message];
@@ -510,6 +497,8 @@
 	};
 
 	const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
+		const messages = createMessagesList(responseMessageId);
+
 		const res = await chatAction(localStorage.token, actionId, {
 			model: modelId,
 			messages: messages.map((m) => ({
@@ -575,6 +564,7 @@
 	const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
 		let _responses = [];
 		console.log('submitPrompt', $chatId);
+		const messages = createMessagesList(history.currentId);
 
 		selectedModels = selectedModels.map((modelId) =>
 			$models.map((m) => m.id).includes(modelId) ? modelId : ''
@@ -668,8 +658,34 @@
 		parentId: string,
 		{ modelId = null, modelIdx = null, newChat = false } = {}
 	) => {
-		let _responses: string[] = [];
+		// Create new chat if newChat is true and first user message
+		if (
+			newChat &&
+			history.messages[history.currentId].parentId === null &&
+			history.messages[history.currentId].role === 'user'
+		) {
+			if (!$temporaryChatEnabled) {
+				chat = await createNewChat(localStorage.token, {
+					id: $chatId,
+					title: $i18n.t('New Chat'),
+					models: selectedModels,
+					system: $settings.system ?? undefined,
+					params: params,
+					history: history,
+					tags: [],
+					timestamp: Date.now()
+				});
+
+				currentChatPage.set(1);
+				await chats.set(await getChatList(localStorage.token, $currentChatPage));
+				await chatId.set(chat.id);
+			} else {
+				await chatId.set('local');
+			}
+			await tick();
+		}
 
+		let _responses: string[] = [];
 		// If modelId is provided, use it, else use selected model
 		let selectedModelIds = modelId
 			? [modelId]
@@ -714,38 +730,14 @@
 		}
 		await tick();
 
-		// Create new chat if only one message in messages
-		if (newChat && messages.length == 2) {
-			if (!$temporaryChatEnabled) {
-				chat = await createNewChat(localStorage.token, {
-					id: $chatId,
-					title: $i18n.t('New Chat'),
-					models: selectedModels,
-					system: $settings.system ?? undefined,
-					params: params,
-					messages: messages,
-					history: history,
-					tags: [],
-					timestamp: Date.now()
-				});
-
-				currentChatPage.set(1);
-				await chats.set(await getChatList(localStorage.token, $currentChatPage));
-				await chatId.set(chat.id);
-			} else {
-				await chatId.set('local');
-			}
-			await tick();
-		}
-
 		const _chatId = JSON.parse(JSON.stringify($chatId));
-
 		await Promise.all(
 			selectedModelIds.map(async (modelId, _modelIdx) => {
 				console.log('modelId', modelId);
 				const model = $models.filter((m) => m.id === modelId).at(0);
 
 				if (model) {
+					const messages = createMessagesList(parentId);
 					// If there are image files, check if model is vision capable
 					const hasImages = messages.some((message) =>
 						message.files?.some((file) => file.type === 'image')
@@ -844,7 +836,7 @@
 						}`
 					}
 				: undefined,
-			...messages
+			...createMessagesList(responseMessageId)
 		]
 			.filter((message) => message?.content?.trim())
 			.map((message) => {
@@ -895,7 +887,7 @@
 				}
 			];
 			files.push(...model.info.meta.knowledge);
-			messages = messages; // Trigger Svelte update
+			history.messages[responseMessageId] = responseMessage;
 		}
 		files.push(
 			...(userMessage?.files ?? []).filter((item) =>
@@ -969,7 +961,7 @@
 					const { value, done } = await reader.read();
 					if (done || stopResponseFlag || _chatId !== $chatId) {
 						responseMessage.done = true;
-						messages = messages;
+						history.messages[responseMessageId] = responseMessage;
 
 						if (stopResponseFlag) {
 							controller.abort('User: Stop Response');
@@ -1036,7 +1028,7 @@
 											);
 										}
 
-										messages = messages;
+										history.messages[responseMessageId] = responseMessage;
 									}
 								} else {
 									responseMessage.done = true;
@@ -1059,7 +1051,8 @@
 										eval_count: data.eval_count,
 										eval_duration: data.eval_duration
 									};
-									messages = messages;
+
+									history.messages[responseMessageId] = responseMessage;
 
 									if ($settings.notificationEnabled && !document.hasFocus()) {
 										const notification = new Notification(`${model.id}`, {
@@ -1128,7 +1121,7 @@
 				);
 			}
 
-			messages = messages;
+			history.messages[responseMessageId] = responseMessage;
 		}
 		await saveChatHandler(_chatId);
 
@@ -1161,7 +1154,8 @@
 			scrollToBottom();
 		}
 
-		if (messages.length == 2 && messages.at(1).content !== '' && selectedModels[0] === model.id) {
+		const messages = createMessagesList(responseMessageId);
+		if (messages.length == 2 && messages.at(-1).content !== '' && selectedModels[0] === model.id) {
 			window.history.replaceState(history.state, '', `/c/${_chatId}`);
 			const _title = await generateChatTitle(userPrompt);
 			await setChatTitle(_chatId, _title);
@@ -1189,7 +1183,7 @@
 				}
 			];
 			files.push(...model.info.meta.knowledge);
-			messages = messages; // Trigger Svelte update
+			history.messages[responseMessageId] = responseMessage;
 		}
 		files.push(
 			...(userMessage?.files ?? []).filter((item) =>
@@ -1240,7 +1234,7 @@
 									}`
 								}
 							: undefined,
-						...messages
+						...createMessagesList(responseMessageId)
 					]
 						.filter((message) => message?.content?.trim())
 						.map((message, idx, arr) => ({
@@ -1318,7 +1312,7 @@
 						}
 						if (done || stopResponseFlag || _chatId !== $chatId) {
 							responseMessage.done = true;
-							messages = messages;
+							history.messages[responseMessageId] = responseMessage;
 
 							if (stopResponseFlag) {
 								controller.abort('User: Stop Response');
@@ -1373,7 +1367,7 @@
 								);
 							}
 
-							messages = messages;
+							history.messages[responseMessageId] = responseMessage;
 						}
 
 						if (autoScroll) {
@@ -1414,7 +1408,7 @@
 
 		await saveChatHandler(_chatId);
 
-		messages = messages;
+		history.messages[responseMessageId] = responseMessage;
 
 		stopResponseFlag = false;
 		await tick();
@@ -1445,9 +1439,9 @@
 			scrollToBottom();
 		}
 
+		const messages = createMessagesList(responseMessageId);
 		if (messages.length == 2 && selectedModels[0] === model.id) {
 			window.history.replaceState(history.state, '', `/c/${_chatId}`);
-
 			const _title = await generateChatTitle(userPrompt);
 			await setChatTitle(_chatId, _title);
 		}
@@ -1497,7 +1491,7 @@
 			);
 		}
 
-		messages = messages;
+		history.messages[responseMessageId] = responseMessage;
 	};
 
 	const stopResponse = () => {
@@ -1508,7 +1502,7 @@
 	const regenerateResponse = async (message) => {
 		console.log('regenerateResponse');
 
-		if (messages.length != 0) {
+		if (history.currentId) {
 			let userMessage = history.messages[message.parentId];
 			let userPrompt = userMessage.content;
 
@@ -1526,11 +1520,11 @@
 		}
 	};
 
-	const continueGeneration = async () => {
-		console.log('continueGeneration');
+	const continueResponse = async () => {
+		console.log('continueResponse');
 		const _chatId = JSON.parse(JSON.stringify($chatId));
 
-		if (messages.length != 0 && messages.at(-1).done == true) {
+		if (history.currentId && history.messages[history.currentId].done == true) {
 			const responseMessage = history.messages[history.currentId];
 			responseMessage.done = false;
 			await tick();
@@ -1558,6 +1552,53 @@
 		}
 	};
 
+	const mergeResponses = async (messageId, responses, _chatId) => {
+		console.log('mergeResponses', messageId, responses);
+		const message = history.messages[messageId];
+		const mergedResponse = {
+			status: true,
+			content: ''
+		};
+		message.merged = mergedResponse;
+		history.messages[messageId] = message;
+
+		try {
+			const [res, controller] = await generateMoACompletion(
+				localStorage.token,
+				message.model,
+				history.messages[message.parentId].content,
+				responses
+			);
+
+			if (res && res.ok && res.body) {
+				const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
+				for await (const update of textStream) {
+					const { value, done, citations, error, usage } = update;
+					if (error || done) {
+						break;
+					}
+
+					if (mergedResponse.content == '' && value == '\n') {
+						continue;
+					} else {
+						mergedResponse.content += value;
+						history.messages[messageId] = message;
+					}
+
+					if (autoScroll) {
+						scrollToBottom();
+					}
+				}
+
+				await saveChatHandler(_chatId);
+			} else {
+				console.error(res);
+			}
+		} catch (e) {
+			console.error(e);
+		}
+	};
+
 	const generateChatTitle = async (userPrompt) => {
 		if ($settings?.title?.auto ?? true) {
 			const title = await generateTitle(
@@ -1600,7 +1641,7 @@
 				description: $i18n.t('Generating search query')
 			}
 		];
-		messages = messages;
+		history.messages[responseMessageId] = responseMessage;
 
 		const prompt = userMessage.content;
 		let searchQuery = await generateSearchQuery(
@@ -1620,7 +1661,7 @@
 				action: 'web_search',
 				description: $i18n.t('No search query generated')
 			});
-			messages = messages;
+			history.messages[responseMessageId] = responseMessage;
 			return;
 		}
 
@@ -1629,7 +1670,7 @@
 			action: 'web_search',
 			description: $i18n.t(`Searching "{{searchQuery}}"`, { searchQuery })
 		});
-		messages = messages;
+		history.messages[responseMessageId] = responseMessage;
 
 		const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => {
 			console.log(error);
@@ -1657,8 +1698,7 @@
 				type: 'web_search_results',
 				urls: results.filenames
 			});
-
-			messages = messages;
+			history.messages[responseMessageId] = responseMessage;
 		} else {
 			responseMessage.statusHistory.push({
 				done: true,
@@ -1666,7 +1706,7 @@
 				action: 'web_search',
 				description: 'No search results found'
 			});
-			messages = messages;
+			history.messages[responseMessageId] = responseMessage;
 		}
 	};
 
@@ -1680,9 +1720,8 @@
 		if ($chatId == _chatId) {
 			if (!$temporaryChatEnabled) {
 				chat = await updateChatById(localStorage.token, _chatId, {
-					messages: messages,
-					history: history,
 					models: selectedModels,
+					history: history,
 					params: params,
 					files: chatFiles
 				});
@@ -1692,52 +1731,6 @@
 			}
 		}
 	};
-	const mergeResponses = async (messageId, responses, _chatId) => {
-		console.log('mergeResponses', messageId, responses);
-		const message = history.messages[messageId];
-		const mergedResponse = {
-			status: true,
-			content: ''
-		};
-		message.merged = mergedResponse;
-		messages = messages;
-
-		try {
-			const [res, controller] = await generateMoACompletion(
-				localStorage.token,
-				message.model,
-				history.messages[message.parentId].content,
-				responses
-			);
-
-			if (res && res.ok && res.body) {
-				const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
-				for await (const update of textStream) {
-					const { value, done, citations, error, usage } = update;
-					if (error || done) {
-						break;
-					}
-
-					if (mergedResponse.content == '' && value == '\n') {
-						continue;
-					} else {
-						mergedResponse.content += value;
-						messages = messages;
-					}
-
-					if (autoScroll) {
-						scrollToBottom();
-					}
-				}
-
-				await saveChatHandler(_chatId);
-			} else {
-				console.error(res);
-			}
-		} catch (e) {
-			console.error(e);
-		}
-	};
 </script>
 
 <svelte:head>
@@ -1788,11 +1781,11 @@
 			/>
 		{/if}
 
-		<Navbar {chat} {title} bind:selectedModels shareEnabled={messages.length > 0} {initNewChat} />
+		<Navbar {chat} {title} bind:selectedModels shareEnabled={!!history.currentId} {initNewChat} />
 
 		<PaneGroup direction="horizontal" class="w-full h-full">
 			<Pane defaultSize={50} class="h-full flex w-full relative">
-				{#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1}
+				{#if $banners.length > 0 && !history.currentId && !$chatId && selectedModels.length <= 1}
 					<div class="absolute top-3 left-0 right-0 w-full z-20">
 						<div class=" flex flex-col gap-1 w-full">
 							{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
@@ -1834,15 +1827,13 @@
 								bind:history
 								bind:autoScroll
 								bind:prompt
-								{messages}
 								{selectedModels}
-								{processing}
 								{sendPrompt}
-								{continueGeneration}
+								{showMessage}
+								{continueResponse}
 								{regenerateResponse}
 								{mergeResponses}
 								{chatActionHandler}
-								{showMessage}
 								bottomPadding={files.length > 0}
 							/>
 						</div>
@@ -1850,13 +1841,13 @@
 
 					<div class="">
 						<MessageInput
+							{history}
 							bind:files
 							bind:prompt
 							bind:autoScroll
 							bind:selectedToolIds
 							bind:webSearchEnabled
 							bind:atSelectedModel
-							{messages}
 							{selectedModels}
 							availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
 								const model = $models.find((m) => m.id === e);

+ 6 - 7
src/lib/components/chat/MessageInput.svelte

@@ -61,15 +61,14 @@
 	let user = null;
 	let chatInputPlaceholder = '';
 
-	export let files = [];
+	export let history;
 
+	export let prompt = '';
+	export let files = [];
 	export let availableToolIds = [];
 	export let selectedToolIds = [];
 	export let webSearchEnabled = false;
 
-	export let prompt = '';
-	export let messages = [];
-
 	let visionCapableModels = [];
 	$: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter(
 		(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
@@ -272,7 +271,7 @@
 	<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
 		<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
 			<div class="relative">
-				{#if autoScroll === false && messages.length > 0}
+				{#if autoScroll === false && history?.currentId}
 					<div
 						class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
 					>
@@ -692,7 +691,7 @@
 								/>
 
 								<div class="self-end mb-2 flex space-x-1 mr-1">
-									{#if messages.length == 0 || messages.at(-1).done == true}
+									{#if !history?.currentId || history.messages[history.currentId]?.done == true}
 										<Tooltip content={$i18n.t('Record voice')}>
 											<button
 												id="voice-input-button"
@@ -744,7 +743,7 @@
 							</div>
 						</div>
 						<div class="flex items-end w-10">
-							{#if messages.length == 0 || messages.at(-1).done == true}
+							{#if !history.currentId || history.messages[history.currentId]?.done == true}
 								{#if prompt === ''}
 									<div class=" flex items-center mb-1">
 										<Tooltip content={$i18n.t('Call')}>

+ 144 - 218
src/lib/components/chat/Messages.svelte

@@ -7,31 +7,45 @@
 	import { getChatList, updateChatById } from '$lib/apis/chats';
 	import { copyToClipboard, findWordIndices } from '$lib/utils';
 
-	import UserMessage from './Messages/UserMessage.svelte';
-	import ResponseMessage from './Messages/ResponseMessage.svelte';
 	import Placeholder from './Messages/Placeholder.svelte';
-	import MultiResponseMessages from './Messages/MultiResponseMessages.svelte';
+	import Message from './Messages/Message.svelte';
 
 	const i18n = getContext('i18n');
 
 	export let chatId = '';
-	export let readOnly = false;
+	export let user = $_user;
+
+	export let prompt;
+	export let history = {};
+	export let selectedModels;
+
+	let messages = [];
+
 	export let sendPrompt: Function;
-	export let continueGeneration: Function;
+	export let continueResponse: Function;
 	export let regenerateResponse: Function;
 	export let mergeResponses: Function;
 	export let chatActionHandler: Function;
 	export let showMessage: Function = () => {};
 
-	export let user = $_user;
-	export let prompt;
-	export let processing = '';
+	export let readOnly = false;
+
 	export let bottomPadding = false;
 	export let autoScroll;
-	export let history = {};
-	export let messages = [];
 
-	export let selectedModels;
+	$: if (history.currentId) {
+		let _messages = [];
+
+		let message = history.messages[history.currentId];
+		while (message) {
+			_messages.unshift({ ...message });
+			message = message.parentId !== null ? history.messages[message.parentId] : null;
+		}
+
+		messages = _messages;
+	} else {
+		messages = [];
+	}
 
 	$: if (autoScroll && bottomPadding) {
 		(async () => {
@@ -45,56 +59,9 @@
 		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, submit = true) => {
-		if (submit) {
-			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
-			};
-
-			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);
-		} else {
-			history.messages[messageId].content = content;
-			await tick();
-			await updateChatById(localStorage.token, chatId, {
-				messages: messages,
-				history: history
-			});
-		}
-	};
-
-	const updateChatMessages = async () => {
+	const updateChatHistory = async () => {
 		await tick();
 		await updateChatById(localStorage.token, chatId, {
-			messages: messages,
 			history: history
 		});
 
@@ -102,49 +69,6 @@
 		await chats.set(await getChatList(localStorage.token, $currentChatPage));
 	};
 
-	const confirmEditResponseMessage = async (messageId, content) => {
-		history.messages[messageId].originalContent = history.messages[messageId].content;
-		history.messages[messageId].content = content;
-
-		await updateChatMessages();
-	};
-
-	const saveNewResponseMessage = async (message, content) => {
-		const responseMessageId = uuidv4();
-		const parentId = message.parentId;
-
-		const responseMessage = {
-			...message,
-			id: responseMessageId,
-			parentId: parentId,
-			childrenIds: [],
-			content: content,
-			timestamp: Math.floor(Date.now() / 1000) // Unix epoch
-		};
-
-		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
-			];
-		}
-
-		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 =
@@ -243,7 +167,89 @@
 		}
 	};
 
-	const deleteMessageHandler = async (messageId) => {
+	const rateMessage = async (messageId, rating) => {
+		history.messages[messageId].annotation = {
+			...history.messages[messageId].annotation,
+			rating: rating
+		};
+
+		await updateChatHistory();
+	};
+
+	const editMessage = async (messageId, content, submit = true) => {
+		if (history.messages[messageId].role === 'user') {
+			if (submit) {
+				// New user message
+				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
+				};
+
+				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);
+			} else {
+				// Edit user message
+				history.messages[messageId].content = content;
+				await updateChatHistory();
+			}
+		} else {
+			if (submit) {
+				// New response message
+				const responseMessageId = uuidv4();
+				const message = history.messages[messageId];
+				const parentId = message.parentId;
+
+				const responseMessage = {
+					...message,
+					id: responseMessageId,
+					parentId: parentId,
+					childrenIds: [],
+					content: content,
+					timestamp: Math.floor(Date.now() / 1000) // Unix epoch
+				};
+
+				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
+					];
+				}
+
+				await updateChatHistory();
+			} else {
+				// Edit response message
+				history.messages[messageId].originalContent = history.messages[messageId].content;
+				history.messages[messageId].content = content;
+				await updateChatHistory();
+			}
+		}
+	};
+
+	const deleteMessage = async (messageId) => {
 		const messageToDelete = history.messages[messageId];
 		const parentMessageId = messageToDelete.parentId;
 		const childMessageIds = messageToDelete.childrenIds ?? [];
@@ -278,15 +284,12 @@
 		showMessage({ id: parentMessageId });
 
 		// Update the chat
-		await updateChatById(localStorage.token, chatId, {
-			messages: messages,
-			history: history
-		});
+		await updateChatHistory();
 	};
 </script>
 
 <div class="h-full flex">
-	{#if messages.length == 0}
+	{#if Object.keys(history?.messages ?? {}).length == 0}
 		<Placeholder
 			modelIds={selectedModels}
 			submitPrompt={async (p) => {
@@ -327,116 +330,39 @@
 	{:else}
 		<div class="w-full pt-2">
 			{#key chatId}
-				{#each messages as message, messageIdx (message.id)}
-					<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 (history.messages[message.parentId]?.models?.length ?? 1) === 1}
-								{#key message.id}
-									<ResponseMessage
-										{message}
-										siblings={history.messages[message.parentId]?.childrenIds ?? []}
-										isLastMessage={messageIdx + 1 === messages.length}
-										{readOnly}
-										{updateChatMessages}
-										{confirmEditResponseMessage}
-										{saveNewResponseMessage}
-										{showPreviousMessage}
-										{showNextMessage}
-										{rateMessage}
-										copyToClipboard={copyToClipboardWithToast}
-										{continueGeneration}
-										{regenerateResponse}
-										on:action={async (e) => {
-											console.log('action', e);
-											if (typeof e.detail === 'string') {
-												await chatActionHandler(chatId, e.detail, message.model, message.id);
-											} else {
-												const { id, event } = e.detail;
-												await chatActionHandler(chatId, id, message.model, message.id, event);
-											}
-										}}
-										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}
-									<MultiResponseMessages
-										bind:history
-										isLastMessage={messageIdx + 1 === messages.length}
-										{messages}
-										{readOnly}
-										{chatId}
-										parentMessage={history.messages[message.parentId]}
-										{messageIdx}
-										{updateChatMessages}
-										{saveNewResponseMessage}
-										{confirmEditResponseMessage}
-										{rateMessage}
-										copyToClipboard={copyToClipboardWithToast}
-										{continueGeneration}
-										{mergeResponses}
-										{regenerateResponse}
-										on:action={async (e) => {
-											console.log('action', e);
-											if (typeof e.detail === 'string') {
-												await chatActionHandler(chatId, e.detail, message.model, message.id);
-											} else {
-												const { id, event } = e.detail;
-												await chatActionHandler(chatId, id, message.model, message.id, event);
-											}
-										}}
-										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}
-
+				<div class="w-full">
+					{#each messages as message, messageIdx (message.id)}
+						<Message
+							{chatId}
+							bind:history
+							messageId={message.id}
+							idx={messageIdx}
+							{user}
+							{showPreviousMessage}
+							{showNextMessage}
+							{editMessage}
+							{deleteMessage}
+							{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>
+				<div class="pb-12" />
 				{#if bottomPadding}
 					<div class="  pb-6" />
 				{/if}

+ 2 - 1
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -226,7 +226,7 @@ __builtins__.input = input`);
 		}
 	};
 
-	$: if (token.raw) {
+	$: if (token) {
 		if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
 			(async () => {
 				await drawMermaidDiagram();
@@ -245,6 +245,7 @@ __builtins__.input = input`);
 	}
 
 	onMount(async () => {
+		console.log('codeblock', lang, code);
 		if (document.documentElement.classList.contains('dark')) {
 			mermaid.initialize({
 				startOnLoad: true,

+ 1 - 1
src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte

@@ -19,7 +19,7 @@
 </script>
 
 <!-- {JSON.stringify(tokens)} -->
-{#each tokens as token, tokenIdx}
+{#each tokens as token, tokenIdx (tokenIdx)}
 	{#if token.type === 'hr'}
 		<hr />
 	{:else if token.type === 'heading'}

+ 166 - 0
src/lib/components/chat/Messages/Message.svelte

@@ -0,0 +1,166 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+
+	import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
+	const dispatch = createEventDispatcher();
+	const i18n = getContext('i18n');
+
+	import { settings } from '$lib/stores';
+	import { copyToClipboard } from '$lib/utils';
+
+	import MultiResponseMessages from './MultiResponseMessages.svelte';
+	import ResponseMessage from './ResponseMessage.svelte';
+	import UserMessage from './UserMessage.svelte';
+	import { updateChatById } from '$lib/apis/chats';
+
+	export let chatId;
+	export let idx = 0;
+
+	export let history;
+	export let messageId;
+
+	export let user;
+
+	export let updateChatHistory;
+	export let chatActionHandler;
+
+	export let showPreviousMessage;
+	export let showNextMessage;
+
+	export let editMessage;
+	export let deleteMessage;
+	export let rateMessage;
+
+	export let regenerateResponse;
+	export let continueResponse;
+
+	// MultiResponseMessages
+	export let mergeResponses;
+
+	export let autoScroll = false;
+	export let readOnly = false;
+
+	onMount(() => {
+		console.log('message', idx);
+	});
+</script>
+
+<div
+	class="flex flex-col justify-between px-5 mb-3 w-full {($settings?.widescreenMode ?? null)
+		? 'max-w-full'
+		: 'max-w-5xl'} mx-auto rounded-lg group"
+>
+	{#if history.messages[messageId]}
+		{#if history.messages[messageId].role === 'user'}
+			<UserMessage
+				{user}
+				{history}
+				{messageId}
+				isFirstMessage={idx === 0}
+				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(messageId)}
+				{readOnly}
+			/>
+		{:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1}
+			<ResponseMessage
+				{history}
+				{messageId}
+				isLastMessage={messageId === history.currentId}
+				siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
+				{showPreviousMessage}
+				{showNextMessage}
+				{editMessage}
+				{rateMessage}
+				{continueResponse}
+				{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 {
+						const { id, event } = e.detail;
+						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
+						});
+					}
+				}}
+				{readOnly}
+			/>
+		{:else}
+			<MultiResponseMessages
+				bind:history
+				{chatId}
+				{messageId}
+				isLastMessage={messageId === history?.currentId}
+				{rateMessage}
+				{editMessage}
+				{continueResponse}
+				{regenerateResponse}
+				{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 {
+						const { id, event } = e.detail;
+						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
+					});
+
+					dispatch('scroll');
+				}}
+				{readOnly}
+			/>
+		{/if}
+	{/if}
+</div>

+ 128 - 123
src/lib/components/chat/Messages/MultiResponseMessages.svelte

@@ -20,39 +20,36 @@
 	const i18n = getContext('i18n');
 
 	export let chatId;
-
 	export let history;
-	export let messages = [];
-	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 continueGeneration: Function;
-	export let mergeResponses: Function;
+	export let continueResponse: Function;
 	export let regenerateResponse: Function;
-	export let saveNewResponseMessage: Function;
+	export let mergeResponses: Function;
 
 	const dispatch = createEventDispatcher();
 
 	let currentMessageId;
-	let groupedMessages = {};
-	let groupedMessagesIdx = {};
-
-	$: if (parentMessage) {
-		initHandler();
+	let parentMessage;
+	let groupedMessageIds = {};
+	let groupedMessageIdsIdx = {};
+
+	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 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;
@@ -67,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;
@@ -87,33 +84,43 @@
 	};
 
 	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,
@@ -126,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 () => {
@@ -141,20 +153,21 @@
 	});
 </script>
 
-<div>
-	<div
-		class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
-		id="responses-container-{chatId}-{parentMessage.id}"
-	>
-		{#key currentMessageId}
-			{#each Object.keys(groupedMessages) as modelIdx}
-				{#if groupedMessagesIdx[modelIdx] !== undefined && groupedMessages[modelIdx].messages.length > 0}
+{#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 message = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]]}
+					{@const _messageId =
+						groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
 
 					<div
-						class=" snap-center w-full max-w-full m-1 border {history.messages[currentMessageId]
+						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]'
@@ -163,17 +176,13 @@
 									$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;
+							if (messageId != _messageId) {
+								let messageChildrenIds = history.messages[_messageId].childrenIds;
 								while (messageChildrenIds.length !== 0) {
 									messageId = messageChildrenIds.at(-1);
-									messageChildrenIds = history.messages[messageId].childrenIds;
+									messageChildrenIds = history.messages[_messageId].childrenIds;
 								}
-								history.currentId = messageId;
+								history.currentId = _messageId;
 								dispatch('change');
 							}
 						}}
@@ -181,96 +190,92 @@
 						{#key history.currentId}
 							{#if message}
 								<ResponseMessage
-									{message}
-									siblings={groupedMessages[modelIdx].messages.map((m) => m.id)}
+									{history}
+									messageId={_messageId}
 									isLastMessage={true}
-									{updateChatMessages}
-									{saveNewResponseMessage}
-									{confirmEditResponseMessage}
+									siblings={groupedMessageIds[modelIdx].messageIds}
 									showPreviousMessage={() => showPreviousMessage(modelIdx)}
 									showNextMessage={() => showNextMessage(modelIdx)}
-									{readOnly}
 									{rateMessage}
-									{copyToClipboard}
-									{continueGeneration}
+									{editMessage}
+									{continueResponse}
 									regenerateResponse={async (message) => {
 										regenerateResponse(message);
 										await tick();
-										groupedMessagesIdx[modelIdx] = groupedMessages[modelIdx].messages.length - 1;
+										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) => {
-										console.log('save', e);
-
-										const message = e.detail;
-										history.messages[message.id] = message;
-										await updateChatById(localStorage.token, chatId, {
-											messages: messages,
-											history: history
-										});
+										dispatch('save', e.detail);
 									}}
+									{readOnly}
 								/>
 							{/if}
 						{/key}
 					</div>
 				{/if}
 			{/each}
-		{/key}
-	</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>
+
+		{#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>
-						</div>
-					{/if}
-				</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 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>
-			</div>
+			{/if}
 		{/if}
-	{/if}
-</div>
+	</div>
+{/if}

+ 31 - 21
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -13,6 +13,7 @@
 	import { synthesizeOpenAISpeech } from '$lib/apis/audio';
 	import { imageGenerations } from '$lib/apis/images';
 	import {
+		copyToClipboard as _copyToClipboard,
 		approximateToHumanReadable,
 		extractParagraphsForAudio,
 		extractSentencesForAudio,
@@ -76,25 +77,30 @@
 		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 updateChatMessages: Function;
-	export let confirmEditResponseMessage: Function;
-	export let saveNewResponseMessage: Function = () => {};
+	export let siblings;
 
 	export let showPreviousMessage: Function;
 	export let showNextMessage: Function;
+
+	export let editMessage: Function;
 	export let rateMessage: Function;
 
-	export let copyToClipboard: Function;
-	export let continueGeneration: Function;
+	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);
 
@@ -111,6 +117,13 @@
 
 	let showRateComment = false;
 
+	const copyToClipboard = async (text) => {
+		const res = await _copyToClipboard(text);
+		if (res) {
+			toast.success($i18n.t('Copying to clipboard was successful!'));
+		}
+	};
+
 	const playAudio = (idx: number) => {
 		return new Promise<void>((res) => {
 			speakingIdx = idx;
@@ -260,11 +273,7 @@
 	};
 
 	const editMessageConfirmHandler = async () => {
-		if (editedContent === '') {
-			editedContent = ' ';
-		}
-
-		confirmEditResponseMessage(message.id, editedContent);
+		editMessage(message.id, editedContent ? editedContent : '', false);
 
 		edit = false;
 		editedContent = '';
@@ -272,8 +281,8 @@
 		await tick();
 	};
 
-	const saveNewMessageHandler = async () => {
-		saveNewResponseMessage(message, editedContent);
+	const saveAsCopyHandler = async () => {
+		editMessage(message.id, editedContent ? editedContent : '');
 
 		edit = false;
 		editedContent = '';
@@ -313,6 +322,8 @@
 	}
 
 	onMount(async () => {
+		console.log('ResponseMessage mounted');
+
 		await tick();
 	});
 </script>
@@ -424,7 +435,7 @@
 											id="save-new-message-button"
 											class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
 											on:click={() => {
-												saveNewMessageHandler();
+												saveAsCopyHandler();
 											}}
 										>
 											{$i18n.t('Save As Copy')}
@@ -909,7 +920,7 @@
 													? '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={() => {
-													continueGeneration();
+													continueResponse();
 
 													(model?.actions ?? [])
 														.filter((action) => action?.__webui__ ?? false)
@@ -1028,8 +1039,7 @@
 							bind:show={showRateComment}
 							bind:message
 							on:submit={(e) => {
-								updateChatMessages();
-
+								dispatch('update');
 								(model?.actions ?? [])
 									.filter((action) => action?.__webui__ ?? false)
 									.forEach((action) => {

+ 39 - 15
src/lib/components/chat/Messages/UserMessage.svelte

@@ -1,38 +1,58 @@
 <script lang="ts">
 	import dayjs from 'dayjs';
+	import { toast } from 'svelte-sonner';
+	import { tick, createEventDispatcher, getContext, onMount } from 'svelte';
+
+	import { models, settings } from '$lib/stores';
+	import { user as _user } from '$lib/stores';
+	import {
+		copyToClipboard as _copyToClipboard,
+		processResponseContent,
+		replaceTokens
+	} from '$lib/utils';
 
-	import { tick, createEventDispatcher, getContext } from 'svelte';
 	import Name from './Name.svelte';
 	import ProfileImage from './ProfileImage.svelte';
-	import { models, settings } from '$lib/stores';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
-
-	import { user as _user } from '$lib/stores';
-	import { getFileContentById } from '$lib/apis/files';
 	import FileItem from '$lib/components/common/FileItem.svelte';
-	import { marked } from 'marked';
-	import { processResponseContent, replaceTokens } from '$lib/utils';
-	import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
 	import Markdown from './Markdown.svelte';
 
 	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 readOnly: boolean;
 
-	export let confirmEditMessage: Function;
 	export let showPreviousMessage: Function;
 	export let showNextMessage: Function;
-	export let copyToClipboard: Function;
+
+	export let editMessage: Function;
+
+	export let isFirstMessage: boolean;
+	export let readOnly: boolean;
 
 	let edit = false;
 	let editedContent = '';
 	let messageEditTextAreaElement: HTMLTextAreaElement;
+
+	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) => {
+		const res = await _copyToClipboard(text);
+		if (res) {
+			toast.success($i18n.t('Copying to clipboard was successful!'));
+		}
+	};
+
 	const editMessageHandler = async () => {
 		edit = true;
 		editedContent = message.content;
@@ -46,7 +66,7 @@
 	};
 
 	const editMessageConfirmHandler = async (submit = true) => {
-		confirmEditMessage(message.id, editedContent, submit);
+		editMessage(message.id, editedContent, submit);
 
 		edit = false;
 		editedContent = '';
@@ -60,6 +80,10 @@
 	const deleteMessageHandler = async () => {
 		dispatch('delete', message.id);
 	};
+
+	onMount(() => {
+		console.log('UserMessage mounted');
+	});
 </script>
 
 <div class=" flex w-full user-message" dir={$settings.chatDirection} id="message-{message.id}">

+ 1 - 1
src/routes/s/[id]/+page.svelte

@@ -155,7 +155,7 @@
 							bind:autoScroll
 							bottomPadding={files.length > 0}
 							sendPrompt={() => {}}
-							continueGeneration={() => {}}
+							continueResponse={() => {}}
 							regenerateResponse={() => {}}
 						/>
 					</div>