import { isValidElement, useEffect, useId, useState, type ReactNode } from "react"; import Markdown, { type Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import { cn } from "../lib/utils"; import { useTheme, THEME_META } from "../context/ThemeContext"; import { mentionChipInlineStyle, parseMentionChipHref } from "../lib/mention-chips"; interface MarkdownBodyProps { children: string; className?: string; /** Optional resolver for relative image paths (e.g. within export packages) */ resolveImageSrc?: (src: string) => string | null; } let mermaidLoaderPromise: Promise | null = null; function loadMermaid() { if (!mermaidLoaderPromise) { mermaidLoaderPromise = import("mermaid").then((module) => module.default); } return mermaidLoaderPromise; } function flattenText(value: ReactNode): string { if (value == null) return ""; if (typeof value === "string" || typeof value === "number") return String(value); if (Array.isArray(value)) return value.map((item) => flattenText(item)).join(""); return ""; } function extractMermaidSource(children: ReactNode): string | null { if (!isValidElement(children)) return null; const childProps = children.props as { className?: unknown; children?: ReactNode }; if (typeof childProps.className !== "string") return null; if (!/\blanguage-mermaid\b/i.test(childProps.className)) return null; return flattenText(childProps.children).replace(/\n$/, ""); } function MermaidDiagramBlock({ source, darkMode }: { source: string; darkMode: boolean }) { const renderId = useId().replace(/[^a-zA-Z0-9_-]/g, ""); const [svg, setSvg] = useState(null); const [error, setError] = useState(null); useEffect(() => { let active = true; setSvg(null); setError(null); loadMermaid() .then(async (mermaid) => { mermaid.initialize({ startOnLoad: false, securityLevel: "strict", theme: darkMode ? "dark" : "default", fontFamily: "inherit", suppressErrorRendering: true, }); const rendered = await mermaid.render(`paperclip-mermaid-${renderId}`, source); if (!active) return; setSvg(rendered.svg); }) .catch((err) => { if (!active) return; const message = err instanceof Error && err.message ? err.message : "Failed to render Mermaid diagram."; setError(message); }); return () => { active = false; }; }, [darkMode, renderId, source]); return (
{svg ? (
) : ( <>

{error ? `Unable to render Mermaid diagram: ${error}` : "Rendering Mermaid diagram..."}

            {source}
          
)}
); } 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 ? parseMentionChipHref(href) : null; if (parsed) { const targetHref = parsed.kind === "project" ? `/projects/${parsed.projectId}` : `/agents/${parsed.agentId}`; return ( {linkChildren} ); } return ( {linkChildren} ); }, }; if (resolveImageSrc) { components.img = ({ node: _node, src, alt, ...imgProps }) => { const resolved = src ? resolveImageSrc(src) : null; return {alt; }; } return (
url}> {children}
); }