File size: 2,560 Bytes
a3ae6ee
c3aa4e3
fc15a4c
8811ee0
4a0bbdf
8811ee0
 
bd8c7a0
 
8811ee0
 
 
 
 
 
 
 
4a0bbdf
8811ee0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a0bbdf
 
 
8811ee0
 
 
 
 
 
 
 
 
 
 
 
 
4a0bbdf
 
8811ee0
 
 
 
 
 
a3ae6ee
 
1b66f8d
a3ae6ee
 
 
 
 
 
 
4a0bbdf
fc15a4c
a3ae6ee
8811ee0
a3ae6ee
 
 
 
 
 
663005a
a3ae6ee
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<script lang="ts">
	import { marked } from 'marked';
	import type { Message } from '$lib/types/Message';
	import { afterUpdate } from 'svelte';
	import { browser } from '$app/environment';

	import CopyToClipBoardBtn from '../CopyToClipBoardBtn.svelte';

	export let message: Message;
	let html = '';
	let el: HTMLElement;

	const renderer = new marked.Renderer();

	// Add wrapper to code blocks
	renderer.code = (code, lang) => {
		return `
			<div class="group code-block">
				<pre>
					<code class="language-${lang}">${code}</code>
				</pre>
			</div>
		`.replaceAll('\t', '');
	};

	const handleParsed = (err: Error | null, parsedHtml: string) => {
		if (err) {
			console.error(err);
		} else {
			html = parsedHtml;
		}
	};

	const options: marked.MarkedOptions = {
		...marked.getDefaults(),
		gfm: true,
		highlight: (code, lang, callback) => {
			import('highlight.js').then(
				({ default: hljs }) => {
					const language = hljs.getLanguage(lang);
					callback?.(null, hljs.highlightAuto(code, language?.aliases).value);
				},
				(err) => {
					console.error(err);
					callback?.(err);
				}
			);
		},
		renderer
	};

	$: browser && marked(message.content, options, handleParsed);

	html = marked(message.content, options);

	afterUpdate(() => {
		if (el) {
			const codeBlocks = el.querySelectorAll('.code-block');

			// Add copy to clipboard button to each code block
			codeBlocks.forEach((block) => {
				if (block.classList.contains('has-copy-btn')) return;

				new CopyToClipBoardBtn({
					target: block,
					props: {
						value: (block as HTMLElement).innerText ?? '',
						classNames:
							'absolute top-2 right-2 invisible opacity-0 group-hover:visible group-hover:opacity-100'
					}
				});
				block.classList.add('has-copy-btn');
			});
		}
	});
</script>

{#if message.from === 'assistant'}
	<div class="flex items-start justify-start gap-4 leading-relaxed">
		<img
			alt=""
			src="https://huggingface.co/avatars/2edb18bd0206c16b433841a47f53fa8e.svg"
			class="mt-5 w-3 h-3 flex-none rounded-full shadow-lg"
		/>
		<div
			class="relative rounded-2xl px-5 py-3.5 border border-gray-100 bg-gradient-to-br from-gray-50 dark:from-gray-800/40 dark:border-gray-800 prose text-gray-600 dark:text-gray-300"
			bind:this={el}
		>
			{@html html}
		</div>
	</div>
{/if}
{#if message.from === 'user'}
	<div class="flex items-start justify-start gap-4">
		<div class="mt-5 w-3 h-3 flex-none rounded-full" />
		<div class="rounded-2xl px-5 py-3.5 text-gray-500 dark:text-gray-400">
			{message.content}
		</div>
	</div>
{/if}