diff --git a/ui/src/components/MarkdownBody.test.tsx b/ui/src/components/MarkdownBody.test.tsx new file mode 100644 index 00000000..06cfc70a --- /dev/null +++ b/ui/src/components/MarkdownBody.test.tsx @@ -0,0 +1,31 @@ +// @vitest-environment node + +import { describe, expect, it } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { ThemeProvider } from "../context/ThemeContext"; +import { MarkdownBody } from "./MarkdownBody"; + +describe("MarkdownBody", () => { + it("renders markdown images without a resolver", () => { + const html = renderToStaticMarkup( + + {"![](/api/attachments/test/content)"} + , + ); + + expect(html).toContain(''); + }); + + it("resolves relative image paths when a resolver is provided", () => { + const html = renderToStaticMarkup( + + `/resolved/${src}`}> + {"![Org chart](images/org-chart.png)"} + + , + ); + + expect(html).toContain('src="/resolved/images/org-chart.png"'); + expect(html).toContain('alt="Org chart"'); + }); +}); diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index 06845527..0fbb52c4 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -1,5 +1,5 @@ import { isValidElement, useEffect, useId, useState, type CSSProperties, type ReactNode } from "react"; -import Markdown from "react-markdown"; +import Markdown, { type Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import { parseProjectMentionHref } from "@paperclipai/shared"; import { cn } from "../lib/utils"; @@ -116,6 +116,42 @@ function MermaidDiagramBlock({ source, darkMode }: { source: string; darkMode: b export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownBodyProps) { const { theme } = useTheme(); + const components: Components = { + pre: ({ node: _node, children: preChildren, ...preProps }) => { + const mermaidSource = extractMermaidSource(preChildren); + if (mermaidSource) { + return ; + } + return
{preChildren}
; + }, + a: ({ href, children: linkChildren }) => { + const parsed = href ? parseProjectMentionHref(href) : null; + if (parsed) { + const label = linkChildren; + return ( + + {label} + + ); + } + return ( + + {linkChildren} + + ); + }, + }; + if (resolveImageSrc) { + components.img = ({ node: _node, src, alt, ...imgProps }) => { + const resolved = src ? resolveImageSrc(src) : null; + return {alt; + }; + } + return (
- { - const mermaidSource = extractMermaidSource(preChildren); - if (mermaidSource) { - return ; - } - return
{preChildren}
; - }, - a: ({ href, children: linkChildren }) => { - const parsed = href ? parseProjectMentionHref(href) : null; - if (parsed) { - const label = linkChildren; - return ( - - {label} - - ); - } - return ( - - {linkChildren} - - ); - }, - img: resolveImageSrc - ? ({ src, alt, ...imgProps }) => { - const resolved = src ? resolveImageSrc(src) : null; - return {alt; - } - : undefined, - }} - > + {children}