| import ReactMarkdown from "react-markdown"; |
| import remarkGfm from "remark-gfm"; |
| import remarkMath from "remark-math"; |
| import rehypeHighlight from "rehype-highlight"; |
| import rehypeKatex from "rehype-katex"; |
| import "katex/dist/katex.min.css"; |
| import Mermaid from "./Mermaid"; |
| function preprocessContent(text) { |
| if (!text) return ""; |
| let processedText = text; |
| const imageBlockRegex = /((?:!\[.*?\]\(.*?\)(?:\s| )*){2,})/g; |
| processedText = processedText.replace(imageBlockRegex, (match) => { |
| return `\n\`\`\`slider\n${match.trim()}\n\`\`\`\n`; |
| }); |
| const parts = processedText.split(/(```[\s\S]*?```|`[^`\n]+`|!\[[\s\S]*?\]\([\s\S]*?\))/g); |
| const processedParts = parts.map(part => { |
| if (part.startsWith("```") || part.startsWith("`") || part.startsWith("![")) return part; |
| let processed = part; |
| const mermaidRegex = /^(?:graph|flowchart|classDiagram|stateDiagram|erDiagram|sequenceDiagram|gantt|pie|quadrantChart|xychart|mindmap|timeline)[\s\S]+?(?=\n\n|\n$|$)/gm; |
| processed = processed.replace(mermaidRegex, (match) => { |
| if (match.trim().split("\n").length > 1) { |
| return `\n\`\`\`mermaid\n${match.trim()}\n\`\`\`\n`; |
| } |
| return match; |
| }); |
|
|
| const treeRegex = /((?:^[ \t]*[βββββββ€βΌ|].*$\n?)+)/gm; |
| processed = processed.replace(treeRegex, (match) => { |
| const lines = match.trim().split("\n"); |
| const hasStrongTreeChars = /[βββββββ€βΌββ]/.test(match); |
| if (lines.length > 1 || hasStrongTreeChars) { |
| return `\n\`\`\`tree\n${match.trim()}\n\`\`\`\n`; |
| } |
| return match; |
| }); |
| processed = processed.replace(/^[ \t\u00a0]*[β’ββΒ·][ \t\u00a0]*/gm, "- "); |
| processed = processed.replace(/\\\[([\s\S]*?)\\\]/g, "$$$$1$$"); |
| processed = processed.replace(/\\\(([\s\S]*?)\\\)/g, "$$1$"); |
| processed = processed.replace(/(\((.+?)\)|\[(.+?)\])/g, (match, full, p1, p2) => { |
| const content = (p1 || p2 || "").trim(); |
| if (!content) return match; |
| const hasMathOperator = /[\\_^={}+\-*/]/.test(content); |
| const isSingleVar = /^[a-z]$/i.test(content); |
| const isExpression = /^[a-z0-9] *[+\-*/=] *[a-z0-9]/i.test(content); |
| if (hasMathOperator || isSingleVar || isExpression) { |
| const delim = (match.startsWith("[") || content.length > 50) ? "$$" : "$"; |
| return `${delim}${match}${delim}`; |
| } |
| return match; |
| }); |
| const lines = processed.split("\n"); |
| const resultLines = lines.map(line => { |
| const trimmed = line.trim(); |
| if ( |
| (trimmed.includes("\\") || /[_^]/.test(trimmed) || /^[a-z] *=/i.test(trimmed)) && |
| !trimmed.includes("$") && |
| !trimmed.startsWith("#") && |
| !trimmed.startsWith("-") && |
| !trimmed.startsWith("*") && |
| trimmed.length > 0 |
| ) { |
| if (/^[a-zA-Z0-9] *=/.test(trimmed) || /^\\[a-zA-Z]+/.test(trimmed)) { |
| return `$$${trimmed}$$`; |
| } |
| } |
| return line; |
| }); |
| return resultLines.join("\n"); |
| }); |
| let finalProcessed = processedParts.join(""); |
| finalProcessed = finalProcessed.replace(/([^\n])\n- /g, "$1\n\n- "); |
| return finalProcessed; |
| } |
| export function AnswerRenderer({ answer }) { |
| const processedAnswer = preprocessContent(answer); |
| return ( |
| <div className="answer-body"> |
| <ReactMarkdown |
| remarkPlugins={[remarkGfm, remarkMath]} |
| rehypePlugins={[rehypeHighlight, rehypeKatex]} |
| components={{ |
| code({ node, inline, className, children, ...props }) { |
| const match = /language-(\w+)/.exec(className || ""); |
| const content = String(children).replace(/\n$/, ""); |
| |
| if (!inline && match?.[1] === "mermaid") { |
| return <Mermaid chart={content} />; |
| } |
| |
| if (!inline && match?.[1] === "slider") { |
| const imageMatches = content.match(/!\[.*?\]\((.*?)\)/g) || []; |
| const imageUrls = imageMatches.map(m => { |
| const urlMatch = m.match(/\((.*?)\)/); |
| return urlMatch ? urlMatch[1] : null; |
| }).filter(Boolean); |
| return ( |
| <div className="image-slider-container"> |
| {imageUrls.map((url, idx) => ( |
| <div key={idx} className="slider-item"> |
| <img src={url} alt={`Image ${idx + 1}`} loading="lazy" /> |
| </div> |
| ))} |
| </div> |
| ); |
| } |
| const isTree = match?.[1] === "tree" || |
| /[βββββββ€βΌββ]/.test(content) || |
| (content.includes("|") && /[ββ]/.test(content)) || |
| /^[ \t]*[|β].*$/m.test(content); |
| if (!inline && isTree) { |
| const handleCopy = () => { |
| navigator.clipboard.writeText(content); |
| }; |
| return ( |
| <div className="chatgpt-style-block"> |
| <div className="block-header"> |
| <span className="block-title">STRUCTURE</span> |
| <button className="copy-button" title="Copy code" onClick={handleCopy}> |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> |
| </button> |
| </div> |
| <pre className="block-content"> |
| <code>{children}</code> |
| </pre> |
| </div> |
| ); |
| } |
| return !inline && match ? ( |
| <pre className={className}> |
| <code className={className} {...props}> |
| {children} |
| </code> |
| </pre> |
| ) : ( |
| <code className={className} {...props}> |
| {children} |
| </code> |
| ); |
| }, |
| }} |
| > |
| {processedAnswer} |
| </ReactMarkdown> |
| </div> |
| ); |
| } |
|
|