| <script lang="ts"> |
| import { marked } from "marked"; |
| import type { Message } from "$lib/types/Message"; |
| import { afterUpdate } from "svelte"; |
| import { deepestChild } from "$lib/utils/deepestChild"; |
| |
| import CodeBlock from "../CodeBlock.svelte"; |
| import IconLoading from "../icons/IconLoading.svelte"; |
| |
| function sanitizeMd(md: string) { |
| return md.replaceAll("&", "&").replaceAll("<", "<"); |
| } |
| function unsanitizeMd(md: string) { |
| return md.replaceAll("<", "<").replaceAll("&", "&"); |
| } |
| |
| export let message: Message; |
| export let loading: boolean = false; |
| |
| let contentEl: HTMLElement; |
| let loadingEl: any; |
| let pendingTimeout: NodeJS.Timeout; |
| |
| const renderer = new marked.Renderer(); |
| |
| |
| renderer.codespan = (code) => { |
| |
| return `<code>${code.replaceAll("&", "&")}</code>`; |
| }; |
| |
| const options: marked.MarkedOptions = { |
| ...marked.getDefaults(), |
| gfm: true, |
| renderer, |
| }; |
| |
| $: tokens = marked.lexer(sanitizeMd(message.content)); |
| |
| afterUpdate(() => { |
| loadingEl?.$destroy(); |
| clearTimeout(pendingTimeout); |
| |
| |
| if (loading) { |
| pendingTimeout = setTimeout(() => { |
| if (contentEl) { |
| loadingEl = new IconLoading({ |
| target: deepestChild(contentEl), |
| props: { classNames: "loading inline ml-2" }, |
| }); |
| } |
| }, 600); |
| } |
| }); |
| </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 prose-pre:my-2 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 text-gray-600 dark:text-gray-300 min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[100px]" |
| > |
| {#if !message.content} |
| <IconLoading classNames="absolute inset-0 m-auto" /> |
| {/if} |
| <div |
| class="prose max-sm:prose-sm dark:prose-invert prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900 prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-headings:font-semibold max-w-none" |
| bind:this={contentEl} |
| > |
| {#each tokens as token} |
| {#if token.type === "code"} |
| <CodeBlock lang={token.lang} code={unsanitizeMd(token.text)} /> |
| {:else} |
| {@html marked.parser([token], options)} |
| {/if} |
| {/each} |
| </div> |
| </div> |
| </div> |
| {/if} |
| {#if message.from === "user"} |
| <div class="flex items-start justify-start gap-4 max-sm:text-sm"> |
| <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 whitespace-break-spaces"> |
| {message.content.trim()} |
| </div> |
| </div> |
| {/if} |
|
|