Spaces:
Sleeping
Sleeping
| 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(/(?<!\d)([,;:])(?=\S)/g, "$1 ") | |
| .replace(/(?<!\d)([.!?])(?=[A-Z])/g, "$1 ") | |
| .replace(/[ \t]{2,}/g, " "); | |
| }); | |
| return normalizedLines | |
| .map((line, index) => { | |
| 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 }) => <p className="my-1.5 leading-6 text-slate-800">{children}</p>, | |
| ul: ({ children }) => <ul className="my-1.5 list-disc space-y-0.5 pl-5">{children}</ul>, | |
| ol: ({ children }) => <ol className="my-1.5 list-decimal space-y-0.5 pl-5">{children}</ol>, | |
| li: ({ children }) => <li className="pl-1 leading-6 text-slate-800">{children}</li>, | |
| strong: ({ children }) => <strong className="font-semibold text-slate-950">{children}</strong>, | |
| h1: ({ children }) => <h1 className="mb-2 mt-4 text-xl font-semibold leading-6 text-slate-950">{children}</h1>, | |
| h2: ({ children }) => <h2 className="mb-2 mt-3 text-lg font-semibold leading-6 text-slate-950">{children}</h2>, | |
| h3: ({ children }) => <h3 className="mb-1.5 mt-3 text-base font-semibold leading-6 text-slate-950">{children}</h3>, | |
| a: ({ children, href }) => ( | |
| <a href={href} className="font-medium text-emerald-700 underline decoration-emerald-200 underline-offset-4" target="_blank" rel="noreferrer"> | |
| {children} | |
| </a> | |
| ), | |
| 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 <PlotlyChartBlock spec={spec} />; | |
| } | |
| } catch { | |
| // fall through to raw code rendering below | |
| } | |
| } | |
| const isBlock = className?.includes("language-"); | |
| if (isBlock) { | |
| return <code className={className}>{children}</code>; | |
| } | |
| return <code className="rounded bg-slate-100 px-1.5 py-0.5 text-[0.85em] text-slate-900">{children}</code>; | |
| }, | |
| pre: ({ children }) => { | |
| const child = Array.isArray(children) ? children[0] : children; | |
| const isChart = isValidElement(child) && child.type === PlotlyChartBlock; | |
| if (isChart) return <>{children}</>; | |
| return <pre className="my-3 max-w-full overflow-x-auto rounded-lg bg-slate-950 p-3 text-xs leading-6 text-slate-50">{children}</pre>; | |
| }, | |
| table: ({ children }) => <SortableMarkdownTable>{children}</SortableMarkdownTable>, | |
| th: ({ children }) => <th className="border border-slate-200 bg-slate-50 px-3 py-2 text-left font-semibold text-slate-900">{children}</th>, | |
| td: ({ children }) => <td className="border border-slate-200 px-3 py-2 align-top text-slate-700">{children}</td>, | |
| }; | |
| export function MarkdownContent({ content }: { content: string }) { | |
| const readableContent = normalizeReadableMarkdown(content); | |
| return ( | |
| <div className="agent-markdown max-w-none text-sm leading-7 text-slate-800 [overflow-wrap:anywhere]"> | |
| <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]} components={markdownComponents}> | |
| {readableContent} | |
| </ReactMarkdown> | |
| </div> | |
| ); | |
| } | |