Messages.svelte 11.5 KB
Newer Older
1
2
<script lang="ts">
	import { v4 as uuidv4 } from 'uuid';
3
	import { chats, config, settings, user as _user, mobile } from '$lib/stores';
4
	import { tick, getContext, onMount } from 'svelte';
5

Jannik Streidl's avatar
Jannik Streidl committed
6
	import { toast } from 'svelte-sonner';
Timothy J. Baek's avatar
Timothy J. Baek committed
7
	import { getChatList, updateChatById } from '$lib/apis/chats';
8

9
10
11
	import UserMessage from './Messages/UserMessage.svelte';
	import ResponseMessage from './Messages/ResponseMessage.svelte';
	import Placeholder from './Messages/Placeholder.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
12
	import Spinner from '../common/Spinner.svelte';
13
	import { imageGenerations } from '$lib/apis/images';
14
	import { copyToClipboard, findWordIndices } from '$lib/utils';
15
16
	import CompareMessages from './Messages/CompareMessages.svelte';
	import { stringify } from 'postcss';
17

18
19
	const i18n = getContext('i18n');

20
	export let chatId = '';
21
	export let readOnly = false;
22
	export let sendPrompt: Function;
Timothy J. Baek's avatar
Timothy J. Baek committed
23
	export let continueGeneration: Function;
24
	export let regenerateResponse: Function;
25
	export let chatActionHandler: Function;
26

27
	export let user = $_user;
Timothy J. Baek's avatar
Timothy J. Baek committed
28
	export let prompt;
Timothy J. Baek's avatar
Timothy J. Baek committed
29
	export let processing = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
30
	export let bottomPadding = false;
31
32
33
34
	export let autoScroll;
	export let history = {};
	export let messages = [];

35
	export let selectedModels;
36

Timothy J. Baek's avatar
Timothy J. Baek committed
37
38
39
	$: if (autoScroll && bottomPadding) {
		(async () => {
			await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
40
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
41
42
43
		})();
	}

Timothy J. Baek's avatar
Timothy J. Baek committed
44
45
46
47
48
	const scrollToBottom = () => {
		const element = document.getElementById('messages-container');
		element.scrollTop = element.scrollHeight;
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
49
50
51
52
	const copyToClipboardWithToast = async (text) => {
		const res = await copyToClipboard(text);
		if (res) {
			toast.success($i18n.t('Copying to clipboard was successful!'));
53
54
55
		}
	};

56
57
	const confirmEditMessage = async (messageId, content) => {
		let userPrompt = content;
58
59
60
61
62
63
64
		let userMessageId = uuidv4();

		let userMessage = {
			id: userMessageId,
			parentId: history.messages[messageId].parentId,
			childrenIds: [],
			role: 'user',
Timothy J. Baek's avatar
Timothy J. Baek committed
65
			content: userPrompt,
66
67
			...(history.messages[messageId].files && { files: history.messages[messageId].files }),
			models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
		};

		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();
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
83
		await sendPrompt(userPrompt, userMessageId);
84
85
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
86
	const updateChatMessages = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
87
88
89
90
91
92
93
		await tick();
		await updateChatById(localStorage.token, chatId, {
			messages: messages,
			history: history
		});

		await chats.set(await getChatList(localStorage.token));
Timothy J. Baek's avatar
Timothy J. Baek committed
94
95
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
96
97
98
99
100
101
102
	const confirmEditResponseMessage = async (messageId, content) => {
		history.messages[messageId].originalContent = history.messages[messageId].content;
		history.messages[messageId].content = content;

		await updateChatMessages();
	};

103
	const rateMessage = async (messageId, rating) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
104
105
106
107
		history.messages[messageId].annotation = {
			...history.messages[messageId].annotation,
			rating: rating
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
108

Timothy J. Baek's avatar
Timothy J. Baek committed
109
		await updateChatMessages();
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
	};

	const showPreviousMessage = async (message) => {
		if (message.parentId !== null) {
			let messageId =
				history.messages[message.parentId].childrenIds[
					Math.max(history.messages[message.parentId].childrenIds.indexOf(message.id) - 1, 0)
				];

			if (message.id !== messageId) {
				let messageChildrenIds = history.messages[messageId].childrenIds;

				while (messageChildrenIds.length !== 0) {
					messageId = messageChildrenIds.at(-1);
					messageChildrenIds = history.messages[messageId].childrenIds;
				}

				history.currentId = messageId;
			}
		} else {
			let childrenIds = Object.values(history.messages)
				.filter((message) => message.parentId === null)
				.map((message) => message.id);
			let messageId = childrenIds[Math.max(childrenIds.indexOf(message.id) - 1, 0)];

			if (message.id !== messageId) {
				let messageChildrenIds = history.messages[messageId].childrenIds;

				while (messageChildrenIds.length !== 0) {
					messageId = messageChildrenIds.at(-1);
					messageChildrenIds = history.messages[messageId].childrenIds;
				}

				history.currentId = messageId;
			}
		}

		await tick();

Zhuoran's avatar
Zhuoran committed
149
		if ($settings?.scrollOnBranchChange ?? true) {
150
151
			const element = document.getElementById('messages-container');
			autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
152

153
154
155
156
			setTimeout(() => {
				scrollToBottom();
			}, 100);
		}
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
	};

	const showNextMessage = async (message) => {
		if (message.parentId !== null) {
			let messageId =
				history.messages[message.parentId].childrenIds[
					Math.min(
						history.messages[message.parentId].childrenIds.indexOf(message.id) + 1,
						history.messages[message.parentId].childrenIds.length - 1
					)
				];

			if (message.id !== messageId) {
				let messageChildrenIds = history.messages[messageId].childrenIds;

				while (messageChildrenIds.length !== 0) {
					messageId = messageChildrenIds.at(-1);
					messageChildrenIds = history.messages[messageId].childrenIds;
				}

				history.currentId = messageId;
			}
		} else {
			let childrenIds = Object.values(history.messages)
				.filter((message) => message.parentId === null)
				.map((message) => message.id);
			let messageId =
				childrenIds[Math.min(childrenIds.indexOf(message.id) + 1, childrenIds.length - 1)];

			if (message.id !== messageId) {
				let messageChildrenIds = history.messages[messageId].childrenIds;

				while (messageChildrenIds.length !== 0) {
					messageId = messageChildrenIds.at(-1);
					messageChildrenIds = history.messages[messageId].childrenIds;
				}

				history.currentId = messageId;
			}
		}

		await tick();

Zhuoran's avatar
Zhuoran committed
200
		if ($settings?.scrollOnBranchChange ?? true) {
201
202
			const element = document.getElementById('messages-container');
			autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
Timothy J. Baek's avatar
Timothy J. Baek committed
203

204
205
206
207
			setTimeout(() => {
				scrollToBottom();
			}, 100);
		}
208
	};
209

Timothy J. Baek's avatar
Timothy J. Baek committed
210
	const deleteMessageHandler = async (messageId) => {
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
211
		const messageToDelete = history.messages[messageId];
Timothy J. Baek's avatar
Timothy J. Baek committed
212
213
214
215
216

		const parentMessageId = messageToDelete.parentId;
		const childMessageIds = messageToDelete.childrenIds ?? [];

		const hasDescendantMessages = childMessageIds.some(
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
217
			(childId) => history.messages[childId]?.childrenIds?.length > 0
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
218
		);
Timothy J. Baek's avatar
Timothy J. Baek committed
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

		history.currentId = parentMessageId;
		await tick();

		// Remove the message itself from the parent message's children array
		history.messages[parentMessageId].childrenIds = history.messages[
			parentMessageId
		].childrenIds.filter((id) => id !== messageId);

		await tick();

		childMessageIds.forEach((childId) => {
			const childMessage = history.messages[childId];

			if (childMessage && childMessage.childrenIds) {
				if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) {
					// If there are no other responses/prompts
					history.messages[parentMessageId].childrenIds = [];
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
237
				} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
238
					childMessage.childrenIds.forEach((grandChildId) => {
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
239
						if (history.messages[grandChildId]) {
Timothy J. Baek's avatar
Timothy J. Baek committed
240
241
							history.messages[grandChildId].parentId = parentMessageId;
							history.messages[parentMessageId].childrenIds.push(grandChildId);
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
242
243
244
245
						}
					});
				}
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
246
247
248
249

			// Remove child message id from the parent message's children array
			history.messages[parentMessageId].childrenIds = history.messages[
				parentMessageId
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
250
251
			].childrenIds.filter((id) => id !== childId);
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
252
253
254

		await tick();

Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
255
256
257
258
		await updateChatById(localStorage.token, chatId, {
			messages: messages,
			history: history
		});
259
	};
260
261
</script>

Timothy J. Baek's avatar
Timothy J. Baek committed
262
<div class="h-full flex">
Timothy J. Baek's avatar
Timothy J. Baek committed
263
264
	{#if messages.length == 0}
		<Placeholder
265
			modelIds={selectedModels}
Timothy J. Baek's avatar
Timothy J. Baek committed
266
			submitPrompt={async (p) => {
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
				let text = p;

				if (p.includes('{{CLIPBOARD}}')) {
					const clipboardText = await navigator.clipboard.readText().catch((err) => {
						toast.error($i18n.t('Failed to read clipboard contents'));
						return '{{CLIPBOARD}}';
					});

					text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
				}

				prompt = text;

				await tick();

				const chatInputElement = document.getElementById('chat-textarea');
				if (chatInputElement) {
Timothy J. Baek's avatar
Timothy J. Baek committed
284
285
					prompt = p;

286
287
288
					chatInputElement.style.height = '';
					chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
					chatInputElement.focus();
Timothy J. Baek's avatar
Timothy J. Baek committed
289

290
291
292
293
294
295
					const words = findWordIndices(prompt);

					if (words.length > 0) {
						const word = words.at(0);
						chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
296
				}
297
298

				await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
299
300
301
			}}
		/>
	{:else}
Timothy J. Baek's avatar
Timothy J. Baek committed
302
		<div class="w-full pt-2">
Timothy J. Baek's avatar
Timothy J. Baek committed
303
304
			{#key chatId}
				{#each messages as message, messageIdx}
Timothy J. Baek's avatar
Timothy J. Baek committed
305
					<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
Timothy J. Baek's avatar
Timothy J. Baek committed
306
						<div
307
							class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null
Timothy J. Baek's avatar
Timothy J. Baek committed
308
								? 'max-w-full'
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
309
								: 'max-w-5xl'} mx-auto rounded-lg group"
Timothy J. Baek's avatar
Timothy J. Baek committed
310
311
312
						>
							{#if message.role === 'user'}
								<UserMessage
Timothy J. Baek's avatar
Timothy J. Baek committed
313
									on:delete={() => deleteMessageHandler(message.id)}
314
									{user}
Timothy J. Baek's avatar
Timothy J. Baek committed
315
316
317
318
319
320
321
322
323
324
325
326
327
									{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}
								/>
328
							{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
Timothy J. Baek's avatar
Timothy J. Baek committed
329
								{#key message.id && history.currentId}
Timothy J. Baek's avatar
Timothy J. Baek committed
330
331
332
333
334
335
336
337
338
339
340
341
342
									<ResponseMessage
										{message}
										siblings={history.messages[message.parentId]?.childrenIds ?? []}
										isLastMessage={messageIdx + 1 === messages.length}
										{readOnly}
										{updateChatMessages}
										{confirmEditResponseMessage}
										{showPreviousMessage}
										{showNextMessage}
										{rateMessage}
										copyToClipboard={copyToClipboardWithToast}
										{continueGeneration}
										{regenerateResponse}
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
343
344
345
										on:action={async (e) => {
											await chatActionHandler(chatId, e.detail, message.model, message.id);
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
346
347
348
349
350
351
352
353
354
355
356
357
										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}
358
359
360
361
362
							{:else}
								{#key message.parentId}
									<CompareMessages
										bind:history
										{messages}
363
										{readOnly}
364
365
366
367
368
369
370
371
372
										{chatId}
										parentMessage={history.messages[message.parentId]}
										{messageIdx}
										{updateChatMessages}
										{confirmEditResponseMessage}
										{rateMessage}
										copyToClipboard={copyToClipboardWithToast}
										{continueGeneration}
										{regenerateResponse}
Timothy J. Baek's avatar
Timothy J. Baek committed
373
374
375
376
377
378
										on:change={async () => {
											await updateChatById(localStorage.token, chatId, {
												messages: messages,
												history: history
											});

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
379
380
381
382
383
384
385
386
											if (autoScroll) {
												const element = document.getElementById('messages-container');
												autoScroll =
													element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
												setTimeout(() => {
													scrollToBottom();
												}, 100);
											}
387
388
389
										}}
									/>
								{/key}
Timothy J. Baek's avatar
Timothy J. Baek committed
390
391
							{/if}
						</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
392
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
393
394
395
				{/each}

				{#if bottomPadding}
Timothy J. Baek's avatar
Timothy J. Baek committed
396
					<div class="  pb-6" />
Timothy J. Baek's avatar
Timothy J. Baek committed
397
398
399
400
401
				{/if}
			{/key}
		</div>
	{/if}
</div>