Cache markdown render output per message part

This commit is contained in:
Shantur Rathore
2025-10-28 14:41:09 +00:00
parent 6597783e85
commit d18e44f721
6 changed files with 140 additions and 63 deletions

View File

@@ -1,72 +1,70 @@
import { createEffect, createSignal, Show } from "solid-js"
import { createEffect, createSignal, onMount, onCleanup } from "solid-js"
import { renderMarkdown } from "../lib/markdown"
import type { TextPart } from "../types/message"
interface MarkdownProps {
content: string
part: TextPart
isDark?: boolean
}
export function Markdown(props: MarkdownProps) {
const [html, setHtml] = createSignal("")
let containerRef: HTMLDivElement | undefined
let latestRequestedText = ""
createEffect(async () => {
const rendered = await renderMarkdown(props.content)
setHtml(rendered)
const part = props.part
const text = part.text || ""
if (part.renderCache && part.renderCache.text === text) {
setHtml(part.renderCache.html)
return
}
latestRequestedText = text
try {
const rendered = await renderMarkdown(text)
if (latestRequestedText === text) {
setHtml(rendered)
part.renderCache = { text, html: rendered }
}
} catch (error) {
console.error("Failed to render markdown:", error)
if (latestRequestedText === text) {
setHtml(text)
}
}
})
createEffect(() => {
const currentHtml = html()
if (containerRef && currentHtml) {
setTimeout(() => {
const codeBlocks = containerRef?.querySelectorAll(".markdown-code-block")
onMount(() => {
const handleClick = async (e: Event) => {
const target = e.target as HTMLElement
const copyButton = target.closest(".code-block-copy") as HTMLButtonElement
codeBlocks?.forEach((block) => {
const existing = block.querySelector(".code-block-header")
if (existing) return
const lang = block.getAttribute("data-language")
const encodedCode = block.getAttribute("data-code")
const header = document.createElement("div")
header.className = "code-block-header"
const languageSpan = lang
? `<span class="code-block-language">${lang}</span>`
: '<span class="code-block-language"></span>'
header.innerHTML = `
${languageSpan}
<button class="code-block-copy" data-code="${encodedCode || ""}">
<svg class="copy-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span class="copy-text">Copy</span>
</button>
`
block.insertBefore(header, block.firstChild)
const button = header.querySelector(".code-block-copy")
if (button) {
button.addEventListener("click", async () => {
const code = button.getAttribute("data-code")
if (code) {
const decodedCode = decodeURIComponent(code)
await navigator.clipboard.writeText(decodedCode)
const copyText = button.querySelector(".copy-text")
if (copyText) {
copyText.textContent = "Copied!"
setTimeout(() => {
copyText.textContent = "Copy"
}, 2000)
}
}
})
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)
}
})
}, 0)
}
}
}
containerRef?.addEventListener("click", handleClick)
onCleanup(() => {
containerRef?.removeEventListener("click", handleClick)
})
})
return <div ref={containerRef} class="prose prose-sm dark:prose-invert max-w-none" innerHTML={html()} />

View File

@@ -25,7 +25,7 @@ export default function MessagePart(props: MessagePartProps) {
<Match when={partType() === "text"}>
<Show when={!props.part.synthetic && props.part.text}>
<div class="message-text">
<Markdown content={props.part.text} isDark={isDark()} />
<Markdown part={props.part} isDark={isDark()} />
</div>
</Show>
</Match>
@@ -48,7 +48,7 @@ export default function MessagePart(props: MessagePartProps) {
</div>
<Show when={isReasoningExpanded()}>
<div class="message-text mt-2">
<Markdown content={props.part.text || ""} isDark={isDark()} />
<Markdown part={props.part} isDark={isDark()} />
</div>
</Show>
</div>

View File

@@ -357,7 +357,7 @@ export default function ToolCall(props: ToolCallProps) {
return (
<div class="tool-call-bash">
<div class="message-text">
<Markdown content={fullOutput} isDark={isDark()} />
<Markdown part={{ type: "text", text: fullOutput }} isDark={isDark()} />
</div>
</div>
)
@@ -384,7 +384,7 @@ export default function ToolCall(props: ToolCallProps) {
if (hasMarkdownCodeBlocks(truncated)) {
return (
<div class="message-text">
<Markdown content={truncated} isDark={isDark()} />
<Markdown part={{ type: "text", text: truncated }} isDark={isDark()} />
</div>
)
}
@@ -479,7 +479,7 @@ export default function ToolCall(props: ToolCallProps) {
if (hasMarkdownCodeBlocks(truncated)) {
return (
<div class="message-text">
<Markdown content={truncated} isDark={isDark()} />
<Markdown part={{ type: "text", text: truncated }} isDark={isDark()} />
</div>
)
}