ResponseMessage.svelte 29.9 KB
Newer Older
1
<script lang="ts">
Jannik Streidl's avatar
Jannik Streidl committed
2
	import { toast } from 'svelte-sonner';
Timothy J. Baek's avatar
Timothy J. Baek committed
3
	import dayjs from 'dayjs';
4
5
6
7
8
	import { marked } from 'marked';
	import tippy from 'tippy.js';
	import auto_render from 'katex/dist/contrib/auto-render.mjs';
	import 'katex/dist/katex.min.css';

9
	import { fade } from 'svelte/transition';
Timothy J. Baek's avatar
Timothy J. Baek committed
10
	import { createEventDispatcher } from 'svelte';
11
12
13
	import { onMount, tick, getContext } from 'svelte';

	const i18n = getContext('i18n');
Timothy J. Baek's avatar
Timothy J. Baek committed
14

Timothy J. Baek's avatar
Timothy J. Baek committed
15
16
17
	const dispatch = createEventDispatcher();

	import { config, settings } from '$lib/stores';
Timothy J. Baek's avatar
Timothy J. Baek committed
18
	import { synthesizeOpenAISpeech } from '$lib/apis/audio';
Timothy J. Baek's avatar
Timothy J. Baek committed
19
	import { imageGenerations } from '$lib/apis/images';
20
	import {
21
		approximateToHumanReadable,
22
23
24
25
		extractSentences,
		revertSanitizedResponseContent,
		sanitizeResponseContent
	} from '$lib/utils';
Timothy J. Baek's avatar
Timothy J. Baek committed
26

27
28
29
	import Name from './Name.svelte';
	import ProfileImage from './ProfileImage.svelte';
	import Skeleton from './Skeleton.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
30
	import CodeBlock from './CodeBlock.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
31
	import Image from '$lib/components/common/Image.svelte';
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
32
	import { WEBUI_BASE_URL } from '$lib/constants';
Timothy J. Baek's avatar
Timothy J. Baek committed
33
	import Tooltip from '$lib/components/common/Tooltip.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
34
	import RateComment from './RateComment.svelte';
35
	import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
36
37
38
39
40
41
42

	export let modelfiles = [];
	export let message;
	export let siblings;

	export let isLastMessage = true;

43
44
	export let readOnly = false;

Timothy J. Baek's avatar
Timothy J. Baek committed
45
	export let updateChatMessages: Function;
46
47
48
49
50
51
	export let confirmEditResponseMessage: Function;
	export let showPreviousMessage: Function;
	export let showNextMessage: Function;
	export let rateMessage: Function;

	export let copyToClipboard: Function;
Timothy J. Baek's avatar
Timothy J. Baek committed
52
	export let continueGeneration: Function;
53
54
55
56
	export let regenerateResponse: Function;

	let edit = false;
	let editedContent = '';
57
	let editTextAreaElement: HTMLTextAreaElement;
58
	let tooltipInstance = null;
59

Timothy J. Baek's avatar
Timothy J. Baek committed
60
	let sentencesAudio = {};
61
	let speaking = null;
Timothy J. Baek's avatar
Timothy J. Baek committed
62
63
	let speakingIdx = null;

64
	let loadingSpeech = false;
65
66
	let generatingImage = false;

Timothy J. Baek's avatar
Timothy J. Baek committed
67
68
	let showRateComment = false;

69
	let showCitations = {};
70
71
	// Backend returns a list of citations per collection, we flatten it to citations per source
	let flattenedCitations = {};
72

73
	$: tokens = marked.lexer(sanitizeResponseContent(message.content));
Timothy J. Baek's avatar
Timothy J. Baek committed
74
75
76
77
78
79
80
81
82
83
84
85
86

	const renderer = new marked.Renderer();

	// For code blocks with simple backticks
	renderer.codespan = (code) => {
		return `<code>${code.replaceAll('&amp;', '&')}</code>`;
	};

	const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		extensions: any;
	};

87
88
89
90
91
92
93
94
	$: if (message) {
		renderStyling();
	}

	const renderStyling = async () => {
		await tick();

		if (tooltipInstance) {
95
			tooltipInstance[0]?.destroy();
96
97
98
99
100
101
		}

		renderLatex();

		if (message.info) {
			tooltipInstance = tippy(`#info-${message.id}`, {
102
				content: `<span class="text-xs" id="tooltip-${message.id}">response_token/s: ${
103
104
105
106
107
108
					`${
						Math.round(
							((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100
						) / 100
					} tokens` ?? 'N/A'
				}<br/>
109
					prompt_token/s: ${
Danny Liu's avatar
Danny Liu committed
110
						Math.round(
Timothy J. Baek's avatar
Timothy J. Baek committed
111
112
113
							((message.info.prompt_eval_count ?? 0) /
								(message.info.prompt_eval_duration / 1000000000)) *
								100
Danny Liu's avatar
Danny Liu committed
114
						) / 100 ?? 'N/A'
115
					} tokens<br/>
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
                    total_duration: ${
											Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ??
											'N/A'
										}ms<br/>
                    load_duration: ${
											Math.round(((message.info.load_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
										}ms<br/>
                    prompt_eval_count: ${message.info.prompt_eval_count ?? 'N/A'}<br/>
                    prompt_eval_duration: ${
											Math.round(((message.info.prompt_eval_duration ?? 0) / 1000000) * 100) /
												100 ?? 'N/A'
										}ms<br/>
                    eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
                    eval_duration: ${
											Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
131
										}ms<br/>
Self Denial's avatar
Self Denial committed
132
133
134
                    approximate_total: ${approximateToHumanReadable(
											message.info.total_duration
										)}</span>`,
135
136
137
				allowHTML: true
			});
		}
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

		if (message.citations) {
			for (const citation of message.citations) {
				const zipped = (citation?.document ?? []).map(function (document, index) {
					return [document, citation.metadata?.[index]];
				});
				for (const [document, metadata] of zipped) {
					const source = metadata?.source ?? 'N/A';
					if (source in flattenedCitations) {
						flattenedCitations[source].document.push(document);
						flattenedCitations[source].metadata.push(metadata);
					} else {
						flattenedCitations[source] = {
							document: [document],
							metadata: [metadata]
						};
					}
				}
			}
			console.log(flattenedCitations);
			console.log(Object.keys(flattenedCitations));
		}
160
161
162
	};

	const renderLatex = () => {
163
164
165
		let chatMessageElements = document
			.getElementById(`message-${message.id}`)
			?.getElementsByClassName('chat-assistant');
166

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
		if (chatMessageElements) {
			for (const element of chatMessageElements) {
				auto_render(element, {
					// customised options
					// • auto-render specific keys, e.g.:
					delimiters: [
						{ left: '$$', right: '$$', display: false },
						{ left: '$ ', right: ' $', display: false },
						{ left: '\\(', right: '\\)', display: false },
						{ left: '\\[', right: '\\]', display: false },
						{ left: '[ ', right: ' ]', display: false }
					],
					// • rendering keys, e.g.:
					throwOnError: false
				});
			}
183
184
185
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
186
187
188
189
190
191
	const playAudio = (idx) => {
		return new Promise((res) => {
			speakingIdx = idx;
			const audio = sentencesAudio[idx];
			audio.play();
			audio.onended = async (e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
192
				await new Promise((r) => setTimeout(r, 300));
Timothy J. Baek's avatar
Timothy J. Baek committed
193
194
195

				if (Object.keys(sentencesAudio).length - 1 === idx) {
					speaking = null;
Timothy J. Baek's avatar
Timothy J. Baek committed
196
197
198
199

					if ($settings.conversationMode) {
						document.getElementById('voice-input-button')?.click();
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
200
201
202
203
204
205
206
				}

				res(e);
			};
		});
	};

207
208
	const toggleSpeakMessage = async () => {
		if (speaking) {
209
210
			try {
				speechSynthesis.cancel();
Timothy J. Baek's avatar
Timothy J. Baek committed
211

212
213
214
				sentencesAudio[speakingIdx].pause();
				sentencesAudio[speakingIdx].currentTime = 0;
			} catch {}
Timothy J. Baek's avatar
Timothy J. Baek committed
215
216
217

			speaking = null;
			speakingIdx = null;
218
219
		} else {
			speaking = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
220

Timothy J. Baek's avatar
Timothy J. Baek committed
221
			if ($settings?.audio?.TTSEngine === 'openai') {
222
				loadingSpeech = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
223

224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
				const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
					const lastIndex = mergedTexts.length - 1;
					if (lastIndex >= 0) {
						const previousText = mergedTexts[lastIndex];
						const wordCount = previousText.split(/\s+/).length;
						if (wordCount < 2) {
							mergedTexts[lastIndex] = previousText + ' ' + currentText;
						} else {
							mergedTexts.push(currentText);
						}
					} else {
						mergedTexts.push(currentText);
					}
					return mergedTexts;
				}, []);

Timothy J. Baek's avatar
Timothy J. Baek committed
240
241
242
243
244
245
246
247
248
249
250
251
				console.log(sentences);

				sentencesAudio = sentences.reduce((a, e, i, arr) => {
					a[i] = null;
					return a;
				}, {});

				let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately

				for (const [idx, sentence] of sentences.entries()) {
					const res = await synthesizeOpenAISpeech(
						localStorage.token,
Timothy J. Baek's avatar
Timothy J. Baek committed
252
						$settings?.audio?.speaker,
Timothy J. Baek's avatar
Timothy J. Baek committed
253
254
255
						sentence
					).catch((error) => {
						toast.error(error);
256
257
258
259

						speaking = null;
						loadingSpeech = false;

Timothy J. Baek's avatar
Timothy J. Baek committed
260
261
262
263
264
265
266
267
268
269
270
						return null;
					});

					if (res) {
						const blob = await res.blob();
						const blobUrl = URL.createObjectURL(blob);
						const audio = new Audio(blobUrl);
						sentencesAudio[idx] = audio;
						loadingSpeech = false;
						lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
271
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
272
273
274
275
276
277
278
279
			} else {
				let voices = [];
				const getVoicesLoop = setInterval(async () => {
					voices = await speechSynthesis.getVoices();
					if (voices.length > 0) {
						clearInterval(getVoicesLoop);

						const voice =
Timothy J. Baek's avatar
Timothy J. Baek committed
280
							voices?.filter((v) => v.name === $settings?.audio?.speaker)?.at(0) ?? undefined;
Timothy J. Baek's avatar
Timothy J. Baek committed
281
282
283
284
285

						const speak = new SpeechSynthesisUtterance(message.content);

						speak.onend = () => {
							speaking = null;
Timothy J. Baek's avatar
Timothy J. Baek committed
286
287
288
							if ($settings.conversationMode) {
								document.getElementById('voice-input-button')?.click();
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
289
290
291
292
293
294
						};
						speak.voice = voice;
						speechSynthesis.speak(speak);
					}
				}, 100);
			}
295
296
297
298
299
300
301
302
303
		}
	};

	const editMessageHandler = async () => {
		edit = true;
		editedContent = message.content;

		await tick();

304
305
		editTextAreaElement.style.height = '';
		editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
306
307
308
	};

	const editMessageConfirmHandler = async () => {
309
310
311
312
		if (editedContent === '') {
			editedContent = ' ';
		}

313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
		confirmEditResponseMessage(message.id, editedContent);

		edit = false;
		editedContent = '';

		await tick();
		renderStyling();
	};

	const cancelEditMessage = async () => {
		edit = false;
		editedContent = '';
		await tick();
		renderStyling();
	};

329
330
	const generateImage = async (message) => {
		generatingImage = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
331
332
333
		const res = await imageGenerations(localStorage.token, message.content).catch((error) => {
			toast.error(error);
		});
334
335
336
		console.log(res);

		if (res) {
Timothy J. Baek's avatar
Timothy J. Baek committed
337
			message.files = res.map((image) => ({
338
				type: 'image',
Timothy J. Baek's avatar
Timothy J. Baek committed
339
				url: `${image.url}`
340
			}));
Timothy J. Baek's avatar
Timothy J. Baek committed
341
342

			dispatch('save', message);
343
344
345
346
347
		}

		generatingImage = false;
	};

348
349
350
351
352
353
	onMount(async () => {
		await tick();
		renderStyling();
	});
</script>

354
{#key message.id}
355
	<div class=" flex w-full message-{message.id}" id="message-{message.id}">
356
		<ProfileImage
Timothy J. Baek's avatar
Timothy J. Baek committed
357
358
			src={modelfiles[message.model]?.imageUrl ??
				($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
359
		/>
360
361
362
363
364
365

		<div class="w-full overflow-hidden">
			<Name>
				{#if message.model in modelfiles}
					{modelfiles[message.model]?.title}
				{:else}
Timothy J. Baek's avatar
Timothy J. Baek committed
366
					{message.model ? ` ${message.model}` : ''}
367
368
369
370
				{/if}

				{#if message.timestamp}
					<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
371
						{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
372
373
374
375
376
377
					</span>
				{/if}
			</Name>

			{#if message.content === ''}
				<Skeleton />
378
			{:else}
379
380
381
382
383
				{#if message.files}
					<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
						{#each message.files as file}
							<div>
								{#if file.type === 'image'}
Timothy J. Baek's avatar
Timothy J. Baek committed
384
									<Image src={file.url} />
385
386
387
388
389
								{/if}
							</div>
						{/each}
					</div>
				{/if}
390
				{#if flattenedCitations}
391
					<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
392
						{#each [...Object.keys(flattenedCitations)] as source}
393
							<div>
394
395
396
397
								<CitationsModal
									bind:show={showCitations[source]}
									citation={flattenedCitations[source]}
								/>
398
399
400
401
								<button
									class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
									type="button"
									on:click={() => {
402
										showCitations[source] = !showCitations[source];
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
									}}
								>
									<div class="p-2.5 bg-red-400 text-white rounded-lg">
										<svg
											xmlns="http://www.w3.org/2000/svg"
											viewBox="0 0 24 24"
											fill="currentColor"
											class="w-6 h-6"
										>
											<path
												fill-rule="evenodd"
												d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
												clip-rule="evenodd"
											/>
											<path
												d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
											/>
										</svg>
									</div>

									<div class="flex flex-col justify-center -space-y-0.5">
										<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
425
											{source}
426
427
428
429
430
431
432
433
434
										</div>

										<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
									</div>
								</button>
							</div>
						{/each}
					</div>
				{/if}
435

436
				<div
Timothy J. Baek's avatar
Timothy J. Baek committed
437
					class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
438
				>
439
440
441
442
443
					<div>
						{#if edit === true}
							<div class=" w-full">
								<textarea
									id="message-edit-{message.id}"
444
									bind:this={editTextAreaElement}
445
446
447
									class=" bg-transparent outline-none w-full resize-none"
									bind:value={editedContent}
									on:input={(e) => {
448
										e.target.style.height = '';
449
										e.target.style.height = `${e.target.scrollHeight}px`;
450
									}}
451
								/>
452

453
								<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
454
									<button
Timothy J. Baek's avatar
Timothy J. Baek committed
455
										class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
456
										on:click={() => {
457
											editMessageConfirmHandler();
458
459
										}}
									>
460
										{$i18n.t('Save')}
461
462
463
									</button>

									<button
464
										class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
465
										on:click={() => {
466
											cancelEditMessage();
467
										}}
468
									>
469
										{$i18n.t('Cancel')}
470
471
472
473
474
475
476
477
									</button>
								</div>
							</div>
						{:else}
							<div class="w-full">
								{#if message?.error === true}
									<div
										class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
478
479
480
481
482
483
484
									>
										<svg
											xmlns="http://www.w3.org/2000/svg"
											fill="none"
											viewBox="0 0 24 24"
											stroke-width="1.5"
											stroke="currentColor"
485
											class="w-5 h-5 self-center"
486
487
488
489
										>
											<path
												stroke-linecap="round"
												stroke-linejoin="round"
490
												d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
491
492
493
											/>
										</svg>

494
495
496
497
498
499
500
										<div class=" self-center">
											{message.content}
										</div>
									</div>
								{:else}
									{#each tokens as token}
										{#if token.type === 'code'}
501
502
503
504
											<CodeBlock
												lang={token.lang}
												code={revertSanitizedResponseContent(token.text)}
											/>
505
506
507
508
509
510
511
512
513
514
515
516
517
										{:else}
											{@html marked.parse(token.raw, {
												...defaults,
												gfm: true,
												breaks: true,
												renderer
											})}
										{/if}
									{/each}
									<!-- {@html marked(message.content.replaceAll('\\', '\\\\'))} -->
								{/if}

								{#if message.done}
518
									<div
519
										class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
520
									>
521
										{#if siblings.length > 1}
Timothy J. Baek's avatar
Timothy J. Baek committed
522
											<div class="flex self-center min-w-fit">
523
												<button
524
													class="self-center dark:hover:text-white hover:text-black transition"
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
													on:click={() => {
														showPreviousMessage(message);
													}}
												>
													<svg
														xmlns="http://www.w3.org/2000/svg"
														viewBox="0 0 20 20"
														fill="currentColor"
														class="w-4 h-4"
													>
														<path
															fill-rule="evenodd"
															d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
															clip-rule="evenodd"
														/>
													</svg>
												</button>

543
												<div class="text-xs font-bold self-center min-w-fit dark:text-gray-100">
544
545
546
547
													{siblings.indexOf(message.id) + 1} / {siblings.length}
												</div>

												<button
548
													class="self-center dark:hover:text-white hover:text-black transition"
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
													on:click={() => {
														showNextMessage(message);
													}}
												>
													<svg
														xmlns="http://www.w3.org/2000/svg"
														viewBox="0 0 20 20"
														fill="currentColor"
														class="w-4 h-4"
													>
														<path
															fill-rule="evenodd"
															d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
															clip-rule="evenodd"
														/>
													</svg>
												</button>
											</div>
										{/if}
568

569
										{#if !readOnly}
570
											<Tooltip content={$i18n.t('Edit')} placement="bottom">
571
572
573
574
575
576
577
												<button
													class="{isLastMessage
														? 'visible'
														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
													on:click={() => {
														editMessageHandler();
													}}
578
												>
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
													<svg
														xmlns="http://www.w3.org/2000/svg"
														fill="none"
														viewBox="0 0 24 24"
														stroke-width="2"
														stroke="currentColor"
														class="w-4 h-4"
													>
														<path
															stroke-linecap="round"
															stroke-linejoin="round"
															d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
														/>
													</svg>
												</button>
											</Tooltip>
										{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
596

597
										<Tooltip content={$i18n.t('Copy')} placement="bottom">
Timothy J. Baek's avatar
Timothy J. Baek committed
598
599
600
601
602
603
604
605
											<button
												class="{isLastMessage
													? 'visible'
													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
												on:click={() => {
													copyToClipboard(message.content);
												}}
											>
606
607
608
609
												<svg
													xmlns="http://www.w3.org/2000/svg"
													fill="none"
													viewBox="0 0 24 24"
610
													stroke-width="2"
611
612
613
614
615
616
													stroke="currentColor"
													class="w-4 h-4"
												>
													<path
														stroke-linecap="round"
														stroke-linejoin="round"
Timothy J. Baek's avatar
Timothy J. Baek committed
617
														d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
618
619
													/>
												</svg>
Timothy J. Baek's avatar
Timothy J. Baek committed
620
621
											</button>
										</Tooltip>
622

623
										{#if !readOnly}
624
											<Tooltip content={$i18n.t('Good Response')} placement="bottom">
625
626
627
												<button
													class="{isLastMessage
														? 'visible'
Timothy J. Baek's avatar
Timothy J. Baek committed
628
629
														: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
														?.rating === 1
630
631
632
633
														? 'bg-gray-100 dark:bg-gray-800'
														: ''} dark:hover:text-white hover:text-black transition"
													on:click={() => {
														rateMessage(message.id, 1);
Timothy J. Baek's avatar
Timothy J. Baek committed
634
														showRateComment = true;
635
636
637
638
639
640

														window.setTimeout(() => {
															document
																.getElementById(`message-feedback-${message.id}`)
																?.scrollIntoView();
														}, 0);
641
													}}
Timothy J. Baek's avatar
Timothy J. Baek committed
642
												>
643
644
645
646
647
648
649
650
651
652
													<svg
														stroke="currentColor"
														fill="none"
														stroke-width="2"
														viewBox="0 0 24 24"
														stroke-linecap="round"
														stroke-linejoin="round"
														class="w-4 h-4"
														xmlns="http://www.w3.org/2000/svg"
													>
653
654
655
656
														<path
															d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
														/>
													</svg>
657
658
												</button>
											</Tooltip>
Timothy J. Baek's avatar
Timothy J. Baek committed
659

660
											<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
661
662
663
												<button
													class="{isLastMessage
														? 'visible'
Timothy J. Baek's avatar
Timothy J. Baek committed
664
665
														: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
														?.rating === -1
666
667
668
669
														? 'bg-gray-100 dark:bg-gray-800'
														: ''} dark:hover:text-white hover:text-black transition"
													on:click={() => {
														rateMessage(message.id, -1);
Timothy J. Baek's avatar
Timothy J. Baek committed
670
														showRateComment = true;
671
672
673
674
675
														window.setTimeout(() => {
															document
																.getElementById(`message-feedback-${message.id}`)
																?.scrollIntoView();
														}, 0);
676
													}}
Timothy J. Baek's avatar
Timothy J. Baek committed
677
												>
678
679
680
681
682
683
684
685
686
687
													<svg
														stroke="currentColor"
														fill="none"
														stroke-width="2"
														viewBox="0 0 24 24"
														stroke-linecap="round"
														stroke-linejoin="round"
														class="w-4 h-4"
														xmlns="http://www.w3.org/2000/svg"
													>
688
689
690
691
														<path
															d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
														/>
													</svg>
692
693
694
												</button>
											</Tooltip>
										{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
695

696
										<Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
Timothy J. Baek's avatar
Timothy J. Baek committed
697
698
											<button
												id="speak-button-{message.id}"
Timothy J. Baek's avatar
Timothy J. Baek committed
699
700
701
702
												class="{isLastMessage
													? 'visible'
													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
												on:click={() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
703
704
													if (!loadingSpeech) {
														toggleSpeakMessage(message);
705
													}
Timothy J. Baek's avatar
Timothy J. Baek committed
706
707
												}}
											>
Timothy J. Baek's avatar
Timothy J. Baek committed
708
												{#if loadingSpeech}
709
710
711
712
713
													<svg
														class=" w-4 h-4"
														fill="currentColor"
														viewBox="0 0 24 24"
														xmlns="http://www.w3.org/2000/svg"
714
715
													>
														<style>
716
717
718
719
															.spinner_S1WN {
																animation: spinner_MGfb 0.8s linear infinite;
																animation-delay: -0.8s;
															}
720

721
722
723
															.spinner_Km9P {
																animation-delay: -0.65s;
															}
724

725
726
727
															.spinner_JApP {
																animation-delay: -0.5s;
															}
728

729
730
731
732
733
734
															@keyframes spinner_MGfb {
																93.75%,
																100% {
																	opacity: 0.2;
																}
															}
735
736
737
738
739
														</style>
														<circle class="spinner_S1WN" cx="4" cy="12" r="3" />
														<circle class="spinner_S1WN spinner_Km9P" cx="12" cy="12" r="3" />
														<circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" />
													</svg>
Timothy J. Baek's avatar
Timothy J. Baek committed
740
741
742
743
744
												{:else if speaking}
													<svg
														xmlns="http://www.w3.org/2000/svg"
														fill="none"
														viewBox="0 0 24 24"
745
														stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
746
747
748
749
750
751
752
753
754
														stroke="currentColor"
														class="w-4 h-4"
													>
														<path
															stroke-linecap="round"
															stroke-linejoin="round"
															d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
														/>
													</svg>
755
756
757
758
759
												{:else}
													<svg
														xmlns="http://www.w3.org/2000/svg"
														fill="none"
														viewBox="0 0 24 24"
760
														stroke-width="2"
761
762
763
764
765
766
														stroke="currentColor"
														class="w-4 h-4"
													>
														<path
															stroke-linecap="round"
															stroke-linejoin="round"
Timothy J. Baek's avatar
Timothy J. Baek committed
767
															d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
768
769
770
														/>
													</svg>
												{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
771
											</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
772
773
										</Tooltip>

774
										{#if $config.images && !readOnly}
Timothy J. Baek's avatar
Timothy J. Baek committed
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
											<Tooltip content="Generate Image" placement="bottom">
												<button
													class="{isLastMessage
														? 'visible'
														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
													on:click={() => {
														if (!generatingImage) {
															generateImage(message);
														}
													}}
												>
													{#if generatingImage}
														<svg
															class=" w-4 h-4"
															fill="currentColor"
															viewBox="0 0 24 24"
															xmlns="http://www.w3.org/2000/svg"
792
793
														>
															<style>
Timothy J. Baek's avatar
Timothy J. Baek committed
794
795
796
797
																.spinner_S1WN {
																	animation: spinner_MGfb 0.8s linear infinite;
																	animation-delay: -0.8s;
																}
798

Timothy J. Baek's avatar
Timothy J. Baek committed
799
800
801
																.spinner_Km9P {
																	animation-delay: -0.65s;
																}
802

Timothy J. Baek's avatar
Timothy J. Baek committed
803
804
805
																.spinner_JApP {
																	animation-delay: -0.5s;
																}
806

Timothy J. Baek's avatar
Timothy J. Baek committed
807
808
809
810
811
812
																@keyframes spinner_MGfb {
																	93.75%,
																	100% {
																		opacity: 0.2;
																	}
																}
813
814
815
816
817
															</style>
															<circle class="spinner_S1WN" cx="4" cy="12" r="3" />
															<circle class="spinner_S1WN spinner_Km9P" cx="12" cy="12" r="3" />
															<circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" />
														</svg>
Timothy J. Baek's avatar
Timothy J. Baek committed
818
819
820
821
822
													{:else}
														<svg
															xmlns="http://www.w3.org/2000/svg"
															fill="none"
															viewBox="0 0 24 24"
823
															stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
824
825
826
827
828
829
830
831
832
833
834
835
															stroke="currentColor"
															class="w-4 h-4"
														>
															<path
																stroke-linecap="round"
																stroke-linejoin="round"
																d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
															/>
														</svg>
													{/if}
												</button>
											</Tooltip>
Timothy J. Baek's avatar
Timothy J. Baek committed
836
837
										{/if}

838
										{#if message.info}
839
											<Tooltip content={$i18n.t('Generation Info')} placement="bottom">
Timothy J. Baek's avatar
Timothy J. Baek committed
840
841
842
843
844
845
846
847
												<button
													class=" {isLastMessage
														? 'visible'
														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
													on:click={() => {
														console.log(message);
													}}
													id="info-{message.id}"
848
												>
Timothy J. Baek's avatar
Timothy J. Baek committed
849
850
851
852
													<svg
														xmlns="http://www.w3.org/2000/svg"
														fill="none"
														viewBox="0 0 24 24"
853
														stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
854
855
856
857
858
859
860
861
862
863
864
														stroke="currentColor"
														class="w-4 h-4"
													>
														<path
															stroke-linecap="round"
															stroke-linejoin="round"
															d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
														/>
													</svg>
												</button>
											</Tooltip>
865
866
										{/if}

867
										{#if isLastMessage && !readOnly}
868
											<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
Timothy J. Baek's avatar
Timothy J. Baek committed
869
870
871
872
873
874
875
876
												<button
													type="button"
													class="{isLastMessage
														? 'visible'
														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
													on:click={() => {
														continueGeneration();
													}}
Timothy J. Baek's avatar
Timothy J. Baek committed
877
												>
Timothy J. Baek's avatar
Timothy J. Baek committed
878
879
880
881
													<svg
														xmlns="http://www.w3.org/2000/svg"
														fill="none"
														viewBox="0 0 24 24"
882
														stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
														stroke="currentColor"
														class="w-4 h-4"
													>
														<path
															stroke-linecap="round"
															stroke-linejoin="round"
															d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
														/>
														<path
															stroke-linecap="round"
															stroke-linejoin="round"
															d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
														/>
													</svg>
												</button>
											</Tooltip>
Timothy J. Baek's avatar
Timothy J. Baek committed
899

900
											<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
Timothy J. Baek's avatar
Timothy J. Baek committed
901
902
903
904
905
906
												<button
													type="button"
													class="{isLastMessage
														? 'visible'
														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
													on:click={regenerateResponse}
907
												>
Timothy J. Baek's avatar
Timothy J. Baek committed
908
909
910
911
													<svg
														xmlns="http://www.w3.org/2000/svg"
														fill="none"
														viewBox="0 0 24 24"
912
														stroke-width="2"
Timothy J. Baek's avatar
Timothy J. Baek committed
913
914
915
916
917
918
919
920
921
922
923
														stroke="currentColor"
														class="w-4 h-4"
													>
														<path
															stroke-linecap="round"
															stroke-linejoin="round"
															d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
														/>
													</svg>
												</button>
											</Tooltip>
924
925
926
										{/if}
									</div>
								{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
927
928
929

								{#if showRateComment}
									<RateComment
930
										messageId={message.id}
Timothy J. Baek's avatar
Timothy J. Baek committed
931
932
933
934
935
936
937
										bind:show={showRateComment}
										bind:message
										on:submit={() => {
											updateChatMessages();
										}}
									/>
								{/if}
938
939
940
							</div>
						{/if}
					</div>
941
				</div>
942
943
			{/if}
		</div>
944
	</div>
945
{/key}
Timothy J. Baek's avatar
Timothy J. Baek committed
946
947
948
949
950
951
952
953
954
955
956

<style>
	.buttons::-webkit-scrollbar {
		display: none; /* for Chrome, Safari and Opera */
	}

	.buttons {
		-ms-overflow-style: none; /* IE and Edge */
		scrollbar-width: none; /* Firefox */
	}
</style>