import { isValidElement } from "react"; import ReactMarkdown from "react-markdown"; import type { Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; import { SortableMarkdownTable } from "./SortableMarkdownTable"; import { PlotlyChartBlock, type PlotlyChartSpec } from "./PlotlyChartBlock"; function repairMarkdownMarkers(line: string) { return line .replace(/([A-Za-z0-9])(?=\*\*)/g, "$1 ") .replace(/(\*\*[^*\n]+\*\*)(?=[A-Za-z0-9])/g, "$1 ") .replace(/\*\*([^*\n]*?)\*\*/g, (_, inner) => `**${inner.trim()}**`); } function normalizeReadableMarkdown(content: string) { const lines = content.replace(/\r\n/g, "\n").split("\n"); let inCodeBlock = false; const normalizedLines = lines.map((line) => { if (line.trimStart().startsWith("```")) { inCodeBlock = !inCodeBlock; return line; } if (inCodeBlock) return line; return repairMarkdownMarkers(line) .replace(/([.!?])\s*-\s*(?=[A-Z0-9*])/g, "$1\n\n- ") .replace(/(^|\n)-(?=\*\*|[A-Z0-9])/g, "$1- ") .replace(/(? { const trimmed = line.trim(); if (!trimmed || /^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) return line; const previous = normalizedLines.slice(0, index).reverse().find((item) => item.trim()); const next = normalizedLines.slice(index + 1).find((item) => item.trim()); const isStandaloneBold = /^\*\*.+\*\*(?:\s+.+)?$/.test(trimmed); const belongsToBoldList = previous?.trim().endsWith(":") || previous?.trim().startsWith("**") || next?.trim().startsWith("**"); return isStandaloneBold && belongsToBoldList ? `- ${trimmed}` : line; }) .join("\n") .replace(/\n{3,}/g, "\n\n") .trim(); } const markdownComponents: Components = { p: ({ children }) =>

{children}

, ul: ({ children }) => , ol: ({ children }) =>
    {children}
, li: ({ children }) =>
  • {children}
  • , strong: ({ children }) => {children}, h1: ({ children }) =>

    {children}

    , h2: ({ children }) =>

    {children}

    , h3: ({ children }) =>

    {children}

    , a: ({ children, href }) => ( {children} ), code: ({ children, className }) => { if (className === "language-plotly") { const raw = String(children).replace(/\n$/, ""); try { const spec = JSON.parse(raw) as PlotlyChartSpec; if (spec?.plotly?.data) { return ; } } catch { // fall through to raw code rendering below } } const isBlock = className?.includes("language-"); if (isBlock) { return {children}; } return {children}; }, pre: ({ children }) => { const child = Array.isArray(children) ? children[0] : children; const isChart = isValidElement(child) && child.type === PlotlyChartBlock; if (isChart) return <>{children}; return
    {children}
    ; }, table: ({ children }) => {children}, th: ({ children }) => {children}, td: ({ children }) => {children}, }; export function MarkdownContent({ content }: { content: string }) { const readableContent = normalizeReadableMarkdown(content); return (
    {readableContent}
    ); }