Spaces:
Running
Running
| <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 | |
| .replace(/<\|[a-z]*$/, "") | |
| .replace(/<\|[a-z]+\|$/, "") | |
| .replace(/<$/, "") | |
| .replaceAll(/<\|[a-z]+\|>/g, " ") | |
| .trim() | |
| .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(); | |
| // For code blocks with simple backticks | |
| renderer.codespan = (code) => { | |
| // Unsanitize double-sanitized 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); | |
| // Add loading animation to the last message if update takes more than 600ms | |
| 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} | |