Timothy J. Baek 9 tháng trước cách đây
mục cha
commit
e6b3de310b

+ 91 - 24
src/lib/components/chat/Messages/MarkdownTokens.svelte

@@ -2,14 +2,20 @@
 	import { marked } from 'marked';
 	import { marked } from 'marked';
 	import type { Token } from 'marked';
 	import type { Token } from 'marked';
 	import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
 	import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
-	import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
+
 	import { onMount } from 'svelte';
 	import { onMount } from 'svelte';
+
+	import Image from '$lib/components/common/Image.svelte';
+	import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
+
 	import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte';
 	import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte';
 
 
 	export let id: string;
 	export let id: string;
 	export let tokens: Token[];
 	export let tokens: Token[];
 	export let top = true;
 	export let top = true;
 
 
+	let containerElement;
+
 	const headerComponent = (depth: number) => {
 	const headerComponent = (depth: number) => {
 		return 'h' + depth;
 		return 'h' + depth;
 	};
 	};
@@ -20,8 +26,61 @@
 		return `<code class="codespan">${code.replaceAll('&amp;', '&')}</code>`;
 		return `<code class="codespan">${code.replaceAll('&amp;', '&')}</code>`;
 	};
 	};
 
 
+	let codes = [];
 	renderer.code = (code, lang) => {
 	renderer.code = (code, lang) => {
-		return `<pre><code class="language-${lang}">${code}</code></pre>`;
+		codes.push({
+			code: code,
+			lang: lang
+		});
+		codes = codes;
+		const codeId = `${id}-${codes.length}`;
+
+		const interval = setInterval(() => {
+			if (document.getElementById(`code-${codeId}`)) {
+				clearInterval(interval);
+
+				new CodeBlock({
+					target: document.getElementById(`code-${codeId}`),
+					props: {
+						id: `${id}-${codes.length}`,
+						lang: lang,
+						code: revertSanitizedResponseContent(code)
+					},
+					hydrate: true,
+					$$inline: true
+				});
+			}
+		}, 10);
+
+		return `<div id="code-${id}-${codes.length}" />`;
+	};
+
+	let images = [];
+	renderer.image = (href, title, text) => {
+		images.push({
+			href: href,
+			title: title,
+			text: text
+		});
+		images = images;
+
+		const imageId = `${id}-${images.length}`;
+
+		const interval = setInterval(() => {
+			if (document.getElementById(`image-${imageId}`)) {
+				clearInterval(interval);
+				new Image({
+					target: document.getElementById(`image-${imageId}`),
+					props: {
+						src: href,
+						alt: text
+					},
+					hydrate: true
+				});
+			}
+		}, 10);
+
+		return `<div id="image-${id}-${images.length}" />`;
 	};
 	};
 
 
 	// Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346)
 	// Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346)
@@ -30,24 +89,31 @@
 		const html = origLinkRenderer.call(renderer, href, title, text);
 		const html = origLinkRenderer.call(renderer, href, title, text);
 		return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ');
 		return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ');
 	};
 	};
+
 	const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
 	const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
 		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		extensions: any;
 		extensions: any;
 	};
 	};
+
+	$: if (tokens) {
+		images = [];
+		codes = [];
+	}
 </script>
 </script>
 
 
-{#each tokens as token, tokenIdx (`${id}-${tokenIdx}`)}
-	{#if token.type === 'code'}
-		{#if token.lang === 'mermaid'}
-			<pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre>
-		{:else}
-			<CodeBlock
-				id={`${id}-${tokenIdx}`}
-				lang={token?.lang ?? ''}
-				code={revertSanitizedResponseContent(token?.text ?? '')}
-			/>
-		{/if}
-		<!-- {:else if token.type === 'heading'}
+<div bind:this={containerElement} class="flex flex-col">
+	{#each tokens as token, tokenIdx (`${id}-${tokenIdx}`)}
+		{#if token.type === 'code'}
+			{#if token.lang === 'mermaid'}
+				<pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre>
+			{:else}
+				<CodeBlock
+					id={`${id}-${tokenIdx}`}
+					lang={token?.lang ?? ''}
+					code={revertSanitizedResponseContent(token?.text ?? '')}
+				/>
+			{/if}
+			<!-- {:else if token.type === 'heading'}
 		<svelte:element this={headerComponent(token.depth)}>
 		<svelte:element this={headerComponent(token.depth)}>
 			<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
 			<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
 		</svelte:element>
 		</svelte:element>
@@ -119,7 +185,7 @@
 			</tbody>
 			</tbody>
 		</table>
 		</table>
 	{:else if token.type === 'text'} -->
 	{:else if token.type === 'text'} -->
-		<!-- {#if top}
+			<!-- {#if top}
 			<p>
 			<p>
 				{#if token.tokens}
 				{#if token.tokens}
 					<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
 					<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
@@ -132,12 +198,13 @@
 		{:else}
 		{:else}
 			{unescapeHtml(token.text)}
 			{unescapeHtml(token.text)}
 		{/if} -->
 		{/if} -->
-	{:else}
-		{@html marked.parse(token.raw, {
-			...defaults,
-			gfm: true,
-			breaks: true,
-			renderer
-		})}
-	{/if}
-{/each}
+		{:else}
+			{@html marked.parse(token.raw, {
+				...defaults,
+				gfm: true,
+				breaks: true,
+				renderer
+			})}
+		{/if}
+	{/each}
+</div>

+ 9 - 14
src/lib/components/common/Image.svelte

@@ -13,18 +13,13 @@
 	let showImagePreview = false;
 	let showImagePreview = false;
 </script>
 </script>
 
 
-<div class={className}>
-	<!-- svelte-ignore a11y-click-events-have-key-events -->
-	<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
-	<img
-		on:click={() => {
-			showImagePreview = true;
-		}}
-		src={_src}
-		{alt}
-		class=" rounded-lg cursor-pointer"
-		draggable="false"
-		data-cy="image"
-	/>
-</div>
+<button
+	class={className}
+	on:click={() => {
+		showImagePreview = true;
+	}}
+>
+	<img src={_src} {alt} class=" rounded-lg cursor-pointer" draggable="false" data-cy="image" />
+</button>
+
 <ImagePreview bind:show={showImagePreview} src={_src} {alt} />
 <ImagePreview bind:show={showImagePreview} src={_src} {alt} />