import { isValidElement, useEffect, useId, useState, type CSSProperties, type ReactNode } from "react"; import Markdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { parseProjectMentionHref } from "@paperclipai/shared"; import { cn } from "../lib/utils"; import { useTheme } from "../context/ThemeContext"; interface MarkdownBodyProps { children: string; className?: string; } 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 hexToRgb(hex: string): { r: number; g: number; b: number } | null { const match = /^#([0-9a-f]{6})$/i.exec(hex.trim()); if (!match) return null; const value = match[1]; return { r: parseInt(value.slice(0, 2), 16), g: parseInt(value.slice(2, 4), 16), b: parseInt(value.slice(4, 6), 16), }; } function mentionChipStyle(color: string | null): CSSProperties | undefined { if (!color) return undefined; const rgb = hexToRgb(color); if (!rgb) return undefined; const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255; return { borderColor: color, backgroundColor: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.22)`, color: luminance > 0.55 ? "#111827" : "#f8fafc", }; } 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 }: MarkdownBodyProps) { const { theme } = useTheme(); 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} ); }, }} > {children}
); }