Files
CodeNomad/src/components/markdown.tsx

112 lines
3.1 KiB
TypeScript

import { createEffect, createSignal, onMount, onCleanup } from "solid-js"
import { renderMarkdown, onLanguagesLoaded, initMarkdown } from "../lib/markdown"
import type { TextPart } from "../types/message"
interface MarkdownProps {
part: TextPart
isDark?: boolean
size?: "base" | "sm" | "tight"
}
export function Markdown(props: MarkdownProps) {
const [html, setHtml] = createSignal("")
let containerRef: HTMLDivElement | undefined
let latestRequestedText = ""
createEffect(async () => {
const part = props.part
const text = part.text || ""
const dark = Boolean(props.isDark)
const themeKey = dark ? "dark" : "light"
latestRequestedText = text
await initMarkdown(dark)
const cache = part.renderCache
if (cache && cache.text === text && cache.theme === themeKey) {
setHtml(cache.html)
return
}
try {
const rendered = await renderMarkdown(text)
if (latestRequestedText === text) {
setHtml(rendered)
part.renderCache = { text, html: rendered, theme: themeKey }
}
} catch (error) {
console.error("Failed to render markdown:", error)
if (latestRequestedText === text) {
setHtml(text)
part.renderCache = { text, html: text, theme: themeKey }
}
}
})
onMount(() => {
const handleClick = async (e: Event) => {
const target = e.target as HTMLElement
const copyButton = target.closest(".code-block-copy") as HTMLButtonElement
if (copyButton) {
e.preventDefault()
const code = copyButton.getAttribute("data-code")
if (code) {
const decodedCode = decodeURIComponent(code)
await navigator.clipboard.writeText(decodedCode)
const copyText = copyButton.querySelector(".copy-text")
if (copyText) {
copyText.textContent = "Copied!"
setTimeout(() => {
copyText.textContent = "Copy"
}, 2000)
}
}
}
}
containerRef?.addEventListener("click", handleClick)
// Register listener for language loading completion
const cleanupLanguageListener = onLanguagesLoaded(async () => {
const part = props.part
const text = part.text || ""
if (latestRequestedText !== text) {
return
}
try {
const rendered = await renderMarkdown(text)
if (latestRequestedText === text) {
setHtml(rendered)
part.renderCache = { text, html: rendered }
}
} catch (error) {
console.error("Failed to re-render markdown after language load:", error)
}
})
onCleanup(() => {
containerRef?.removeEventListener("click", handleClick)
cleanupLanguageListener()
})
})
const proseClass = () => {
const classes = ["prose", "dark:prose-invert", "max-w-none"]
if (props.size === "tight") {
classes.push("prose-sm", "prose-tight")
} else if (props.size === "sm") {
classes.push("prose-sm")
}
return classes.join(" ")
}
return <div ref={containerRef} class={proseClass()} innerHTML={html()} />
}