| /** | |||||
| * @fileoverview AudioBlock component for rendering audio elements in Markdown. | |||||
| * Extracted from the main markdown renderer for modularity. | |||||
| * Uses the AudioGallery component to display audio players. | |||||
| */ | |||||
| import React, { memo } from 'react' | |||||
| import AudioGallery from '@/app/components/base/audio-gallery' | |||||
| const AudioBlock: any = memo(({ node }: any) => { | |||||
| const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src) | |||||
| if (srcs.length === 0) { | |||||
| const src = node.properties?.src | |||||
| if (src) | |||||
| return <AudioGallery key={src} srcs={[src]} /> | |||||
| return null | |||||
| } | |||||
| return <AudioGallery key={srcs.join()} srcs={srcs} /> | |||||
| }) | |||||
| AudioBlock.displayName = 'AudioBlock' | |||||
| export default AudioBlock |
| import ReactMarkdown from 'react-markdown' | |||||
| import { memo, useEffect, useMemo, useRef, useState } from 'react' | |||||
| import ReactEcharts from 'echarts-for-react' | import ReactEcharts from 'echarts-for-react' | ||||
| import 'katex/dist/katex.min.css' | |||||
| import RemarkMath from 'remark-math' | |||||
| import RemarkBreaks from 'remark-breaks' | |||||
| import RehypeKatex from 'rehype-katex' | |||||
| import RemarkGfm from 'remark-gfm' | |||||
| import RehypeRaw from 'rehype-raw' | |||||
| import SyntaxHighlighter from 'react-syntax-highlighter' | import SyntaxHighlighter from 'react-syntax-highlighter' | ||||
| import { | import { | ||||
| atelierHeathDark, | atelierHeathDark, | ||||
| atelierHeathLight, | atelierHeathLight, | ||||
| } from 'react-syntax-highlighter/dist/esm/styles/hljs' | } from 'react-syntax-highlighter/dist/esm/styles/hljs' | ||||
| import { Component, memo, useEffect, useMemo, useRef, useState } from 'react' | |||||
| import { flow } from 'lodash-es' | |||||
| import ActionButton from '@/app/components/base/action-button' | import ActionButton from '@/app/components/base/action-button' | ||||
| import CopyIcon from '@/app/components/base/copy-icon' | import CopyIcon from '@/app/components/base/copy-icon' | ||||
| import SVGBtn from '@/app/components/base/svg' | import SVGBtn from '@/app/components/base/svg' | ||||
| import Flowchart from '@/app/components/base/mermaid' | import Flowchart from '@/app/components/base/mermaid' | ||||
| import ImageGallery from '@/app/components/base/image-gallery' | |||||
| import { useChatContext } from '@/app/components/base/chat/chat/context' | |||||
| import VideoGallery from '@/app/components/base/video-gallery' | |||||
| import AudioGallery from '@/app/components/base/audio-gallery' | |||||
| import MarkdownButton from '@/app/components/base/markdown-blocks/button' | |||||
| import MarkdownForm from '@/app/components/base/markdown-blocks/form' | |||||
| import MarkdownMusic from '@/app/components/base/markdown-blocks/music' | |||||
| import ThinkBlock from '@/app/components/base/markdown-blocks/think-block' | |||||
| import { Theme } from '@/types/app' | import { Theme } from '@/types/app' | ||||
| import useTheme from '@/hooks/use-theme' | import useTheme from '@/hooks/use-theme' | ||||
| import cn from '@/utils/classnames' | |||||
| import SVGRenderer from './svg-gallery' | |||||
| import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory | |||||
| import MarkdownMusic from '@/app/components/base/markdown-blocks/music' | |||||
| import ErrorBoundary from '@/app/components/base/markdown/error-boundary' | |||||
| // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD | // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD | ||||
| const capitalizationLanguageNameMap: Record<string, string> = { | const capitalizationLanguageNameMap: Record<string, string> = { | ||||
| return language.charAt(0).toUpperCase() + language.substring(1) | return language.charAt(0).toUpperCase() + language.substring(1) | ||||
| } | } | ||||
| const preprocessLaTeX = (content: string) => { | |||||
| if (typeof content !== 'string') | |||||
| return content | |||||
| const codeBlockRegex = /```[\s\S]*?```/g | |||||
| const codeBlocks = content.match(codeBlockRegex) || [] | |||||
| let processedContent = content.replace(codeBlockRegex, 'CODE_BLOCK_PLACEHOLDER') | |||||
| processedContent = flow([ | |||||
| (str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`), | |||||
| (str: string) => str.replace(/\\\[([\s\S]*?)\\\]/g, (_, equation) => `$$${equation}$$`), | |||||
| (str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`), | |||||
| (str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`), | |||||
| ])(processedContent) | |||||
| codeBlocks.forEach((block) => { | |||||
| processedContent = processedContent.replace('CODE_BLOCK_PLACEHOLDER', block) | |||||
| }) | |||||
| return processedContent | |||||
| } | |||||
| const preprocessThinkTag = (content: string) => { | |||||
| const thinkOpenTagRegex = /<think>\n/g | |||||
| const thinkCloseTagRegex = /\n<\/think>/g | |||||
| return flow([ | |||||
| (str: string) => str.replace(thinkOpenTagRegex, '<details data-think=true>\n'), | |||||
| (str: string) => str.replace(thinkCloseTagRegex, '\n[ENDTHINKFLAG]</details>'), | |||||
| ])(content) | |||||
| } | |||||
| export function PreCode(props: { children: any }) { | |||||
| const ref = useRef<HTMLPreElement>(null) | |||||
| return ( | |||||
| <pre ref={ref}> | |||||
| <span | |||||
| className="copy-code-button" | |||||
| ></span> | |||||
| {props.children} | |||||
| </pre> | |||||
| ) | |||||
| } | |||||
| // **Add code block | // **Add code block | ||||
| // Avoid error #185 (Maximum update depth exceeded. | // Avoid error #185 (Maximum update depth exceeded. | ||||
| // This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. | // This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. | ||||
| }) | }) | ||||
| CodeBlock.displayName = 'CodeBlock' | CodeBlock.displayName = 'CodeBlock' | ||||
| const VideoBlock: any = memo(({ node }: any) => { | |||||
| const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src) | |||||
| if (srcs.length === 0) { | |||||
| const src = node.properties?.src | |||||
| if (src) | |||||
| return <VideoGallery key={src} srcs={[src]} /> | |||||
| return null | |||||
| } | |||||
| return <VideoGallery key={srcs.join()} srcs={srcs} /> | |||||
| }) | |||||
| VideoBlock.displayName = 'VideoBlock' | |||||
| const AudioBlock: any = memo(({ node }: any) => { | |||||
| const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src) | |||||
| if (srcs.length === 0) { | |||||
| const src = node.properties?.src | |||||
| if (src) | |||||
| return <AudioGallery key={src} srcs={[src]} /> | |||||
| return null | |||||
| } | |||||
| return <AudioGallery key={srcs.join()} srcs={srcs} /> | |||||
| }) | |||||
| AudioBlock.displayName = 'AudioBlock' | |||||
| const ScriptBlock = memo(({ node }: any) => { | |||||
| const scriptContent = node.children[0]?.value || '' | |||||
| return `<script>${scriptContent}</script>` | |||||
| }) | |||||
| ScriptBlock.displayName = 'ScriptBlock' | |||||
| const Paragraph = (paragraph: any) => { | |||||
| const { node }: any = paragraph | |||||
| const children_node = node.children | |||||
| if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') { | |||||
| return ( | |||||
| <div className="markdown-img-wrapper"> | |||||
| <ImageGallery srcs={[children_node[0].properties.src]} /> | |||||
| { | |||||
| Array.isArray(paragraph.children) && paragraph.children.length > 1 && ( | |||||
| <div className="mt-2">{paragraph.children.slice(1)}</div> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| return <p>{paragraph.children}</p> | |||||
| } | |||||
| const Img = ({ src }: any) => { | |||||
| return <div className="markdown-img-wrapper"><ImageGallery srcs={[src]} /></div> | |||||
| } | |||||
| const Link = ({ node, children, ...props }: any) => { | |||||
| if (node.properties?.href && node.properties.href?.toString().startsWith('abbr')) { | |||||
| // eslint-disable-next-line react-hooks/rules-of-hooks | |||||
| const { onSend } = useChatContext() | |||||
| const hidden_text = decodeURIComponent(node.properties.href.toString().split('abbr:')[1]) | |||||
| return <abbr className="cursor-pointer underline !decoration-primary-700 decoration-dashed" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr> | |||||
| } | |||||
| else { | |||||
| return <a {...props} target="_blank" className="cursor-pointer underline !decoration-primary-700 decoration-dashed">{children || 'Download'}</a> | |||||
| } | |||||
| } | |||||
| export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) { | |||||
| const latexContent = flow([ | |||||
| preprocessThinkTag, | |||||
| preprocessLaTeX, | |||||
| ])(props.content) | |||||
| return ( | |||||
| <div className={cn('markdown-body', '!text-text-primary', props.className)}> | |||||
| <ReactMarkdown | |||||
| remarkPlugins={[ | |||||
| RemarkGfm, | |||||
| [RemarkMath, { singleDollarTextMath: false }], | |||||
| RemarkBreaks, | |||||
| ]} | |||||
| rehypePlugins={[ | |||||
| RehypeKatex, | |||||
| RehypeRaw as any, | |||||
| // The Rehype plug-in is used to remove the ref attribute of an element | |||||
| () => { | |||||
| return (tree) => { | |||||
| const iterate = (node: any) => { | |||||
| if (node.type === 'element' && node.properties?.ref) | |||||
| delete node.properties.ref | |||||
| if (node.type === 'element' && !/^[a-z][a-z0-9]*$/i.test(node.tagName)) { | |||||
| node.type = 'text' | |||||
| node.value = `<${node.tagName}` | |||||
| } | |||||
| if (node.children) | |||||
| node.children.forEach(iterate) | |||||
| } | |||||
| tree.children.forEach(iterate) | |||||
| } | |||||
| }, | |||||
| ]} | |||||
| disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]} | |||||
| components={{ | |||||
| code: CodeBlock, | |||||
| img: Img, | |||||
| video: VideoBlock, | |||||
| audio: AudioBlock, | |||||
| a: Link, | |||||
| p: Paragraph, | |||||
| button: MarkdownButton, | |||||
| form: MarkdownForm, | |||||
| script: ScriptBlock as any, | |||||
| details: ThinkBlock, | |||||
| }} | |||||
| > | |||||
| {/* Markdown detect has problem. */} | |||||
| {latexContent} | |||||
| </ReactMarkdown> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| // **Add an ECharts runtime error handler | |||||
| // Avoid error #7832 (Crash when ECharts accesses undefined objects) | |||||
| // This can happen when a component attempts to access an undefined object that references an unregistered map, causing the program to crash. | |||||
| export default class ErrorBoundary extends Component { | |||||
| constructor(props: any) { | |||||
| super(props) | |||||
| this.state = { hasError: false } | |||||
| } | |||||
| componentDidCatch(error: any, errorInfo: any) { | |||||
| this.setState({ hasError: true }) | |||||
| console.error(error, errorInfo) | |||||
| } | |||||
| render() { | |||||
| // eslint-disable-next-line ts/ban-ts-comment | |||||
| // @ts-expect-error | |||||
| if (this.state.hasError) | |||||
| return <div>Oops! An error occurred. This could be due to an ECharts runtime error or invalid SVG content. <br />(see the browser console for more information)</div> | |||||
| // eslint-disable-next-line ts/ban-ts-comment | |||||
| // @ts-expect-error | |||||
| return this.props.children | |||||
| } | |||||
| } | |||||
| export default CodeBlock |
| /** | |||||
| * @fileoverview Img component for rendering <img> tags in Markdown. | |||||
| * Extracted from the main markdown renderer for modularity. | |||||
| * Uses the ImageGallery component to display images. | |||||
| */ | |||||
| import React from 'react' | |||||
| import ImageGallery from '@/app/components/base/image-gallery' | |||||
| const Img = ({ src }: any) => { | |||||
| return <div className="markdown-img-wrapper"><ImageGallery srcs={[src]} /></div> | |||||
| } | |||||
| export default Img |
| /** | |||||
| * @fileoverview Barrel file for all markdown block components. | |||||
| * This allows for cleaner imports in other parts of the application. | |||||
| */ | |||||
| export { default as AudioBlock } from './audio-block' | |||||
| export { default as CodeBlock } from './code-block' | |||||
| export { default as Img } from './img' | |||||
| export { default as Link } from './link' | |||||
| export { default as Paragraph } from './paragraph' | |||||
| export { default as PreCode } from './pre-code' | |||||
| export { default as ScriptBlock } from './script-block' | |||||
| export { default as VideoBlock } from './video-block' | |||||
| // Assuming these are also standalone components in this directory intended for Markdown rendering | |||||
| export { default as MarkdownButton } from './button' | |||||
| export { default as MarkdownForm } from './form' | |||||
| export { default as ThinkBlock } from './think-block' |
| /** | |||||
| * @fileoverview Link component for rendering <a> tags in Markdown. | |||||
| * Extracted from the main markdown renderer for modularity. | |||||
| * Handles special rendering for "abbr:" type links for interactive chat actions. | |||||
| */ | |||||
| import React from 'react' | |||||
| import { useChatContext } from '@/app/components/base/chat/chat/context' | |||||
| const Link = ({ node, children, ...props }: any) => { | |||||
| const { onSend } = useChatContext() | |||||
| if (node.properties?.href && node.properties.href?.toString().startsWith('abbr')) { | |||||
| const hidden_text = decodeURIComponent(node.properties.href.toString().split('abbr:')[1]) | |||||
| return <abbr className="cursor-pointer underline !decoration-primary-700 decoration-dashed" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr> | |||||
| } | |||||
| else { | |||||
| return <a {...props} target="_blank" className="cursor-pointer underline !decoration-primary-700 decoration-dashed">{children || 'Download'}</a> | |||||
| } | |||||
| } | |||||
| export default Link |
| /** | |||||
| * @fileoverview Paragraph component for rendering <p> tags in Markdown. | |||||
| * Extracted from the main markdown renderer for modularity. | |||||
| * Handles special rendering for paragraphs that directly contain an image. | |||||
| */ | |||||
| import React from 'react' | |||||
| import ImageGallery from '@/app/components/base/image-gallery' | |||||
| const Paragraph = (paragraph: any) => { | |||||
| const { node }: any = paragraph | |||||
| const children_node = node.children | |||||
| if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') { | |||||
| return ( | |||||
| <div className="markdown-img-wrapper"> | |||||
| <ImageGallery srcs={[children_node[0].properties.src]} /> | |||||
| { | |||||
| Array.isArray(paragraph.children) && paragraph.children.length > 1 && ( | |||||
| <div className="mt-2">{paragraph.children.slice(1)}</div> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| return <p>{paragraph.children}</p> | |||||
| } | |||||
| export default Paragraph |
| /** | |||||
| * @fileoverview PreCode component for rendering <pre> tags in Markdown. | |||||
| * Extracted from the main markdown renderer for modularity. | |||||
| * This is a simple wrapper around the HTML <pre> element. | |||||
| */ | |||||
| import React, { useRef } from 'react' | |||||
| function PreCode(props: { children: any }) { | |||||
| const ref = useRef<HTMLPreElement>(null) | |||||
| return ( | |||||
| <pre ref={ref}> | |||||
| <span | |||||
| className="copy-code-button" | |||||
| ></span> | |||||
| {props.children} | |||||
| </pre> | |||||
| ) | |||||
| } | |||||
| export default PreCode |
| /** | |||||
| * @fileoverview ScriptBlock component for handling <script> tags in Markdown. | |||||
| * Extracted from the main markdown renderer for modularity. | |||||
| * Note: Current implementation returns the script tag as a string, which might not execute as expected in React. | |||||
| * This behavior is preserved from the original implementation and may need review for security and functionality. | |||||
| */ | |||||
| import { memo } from 'react' | |||||
| const ScriptBlock = memo(({ node }: any) => { | |||||
| const scriptContent = node.children[0]?.value || '' | |||||
| return `<script>${scriptContent}</script>` | |||||
| }) | |||||
| ScriptBlock.displayName = 'ScriptBlock' | |||||
| export default ScriptBlock |
| /** | |||||
| * @fileoverview VideoBlock component for rendering video elements in Markdown. | |||||
| * Extracted from the main markdown renderer for modularity. | |||||
| * Uses the VideoGallery component to display videos. | |||||
| */ | |||||
| import React, { memo } from 'react' | |||||
| import VideoGallery from '@/app/components/base/video-gallery' | |||||
| const VideoBlock: any = memo(({ node }: any) => { | |||||
| const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src) | |||||
| if (srcs.length === 0) { | |||||
| const src = node.properties?.src | |||||
| if (src) | |||||
| return <VideoGallery key={src} srcs={[src]} /> | |||||
| return null | |||||
| } | |||||
| return <VideoGallery key={srcs.join()} srcs={srcs} /> | |||||
| }) | |||||
| VideoBlock.displayName = 'VideoBlock' | |||||
| export default VideoBlock |
| /** | |||||
| * @fileoverview ErrorBoundary component for React. | |||||
| * This component was extracted from the main markdown renderer. | |||||
| * It catches JavaScript errors anywhere in its child component tree, | |||||
| * logs those errors, and displays a fallback UI instead of the crashed component tree. | |||||
| * Primarily used around complex rendering logic like ECharts or SVG within Markdown. | |||||
| */ | |||||
| import React, { Component } from 'react' | |||||
| // **Add an ECharts runtime error handler | |||||
| // Avoid error #7832 (Crash when ECharts accesses undefined objects) | |||||
| // This can happen when a component attempts to access an undefined object that references an unregistered map, causing the program to crash. | |||||
| export default class ErrorBoundary extends Component { | |||||
| constructor(props: any) { | |||||
| super(props) | |||||
| this.state = { hasError: false } | |||||
| } | |||||
| componentDidCatch(error: any, errorInfo: any) { | |||||
| this.setState({ hasError: true }) | |||||
| console.error(error, errorInfo) | |||||
| } | |||||
| render() { | |||||
| // eslint-disable-next-line ts/ban-ts-comment | |||||
| // @ts-expect-error | |||||
| if (this.state.hasError) | |||||
| return <div>Oops! An error occurred. This could be due to an ECharts runtime error or invalid SVG content. <br />(see the browser console for more information)</div> | |||||
| // eslint-disable-next-line ts/ban-ts-comment | |||||
| // @ts-expect-error | |||||
| return this.props.children | |||||
| } | |||||
| } |
| import ReactMarkdown from 'react-markdown' | |||||
| import 'katex/dist/katex.min.css' | |||||
| import RemarkMath from 'remark-math' | |||||
| import RemarkBreaks from 'remark-breaks' | |||||
| import RehypeKatex from 'rehype-katex' | |||||
| import RemarkGfm from 'remark-gfm' | |||||
| import RehypeRaw from 'rehype-raw' | |||||
| import { flow } from 'lodash-es' | |||||
| import cn from '@/utils/classnames' | |||||
| import { preprocessLaTeX, preprocessThinkTag } from './markdown-utils' | |||||
| import { | |||||
| AudioBlock, | |||||
| CodeBlock, | |||||
| Img, | |||||
| Link, | |||||
| MarkdownButton, | |||||
| MarkdownForm, | |||||
| Paragraph, | |||||
| ScriptBlock, | |||||
| ThinkBlock, | |||||
| VideoBlock, | |||||
| } from '@/app/components/base/markdown-blocks' | |||||
| /** | |||||
| * @fileoverview Main Markdown rendering component. | |||||
| * This file was refactored to extract individual block renderers and utility functions | |||||
| * into separate modules for better organization and maintainability as of [Date of refactor]. | |||||
| * Further refactoring candidates (custom block components not fitting general categories) | |||||
| * are noted in their respective files if applicable. | |||||
| */ | |||||
| export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) { | |||||
| const latexContent = flow([ | |||||
| preprocessThinkTag, | |||||
| preprocessLaTeX, | |||||
| ])(props.content) | |||||
| return ( | |||||
| <div className={cn('markdown-body', '!text-text-primary', props.className)}> | |||||
| <ReactMarkdown | |||||
| remarkPlugins={[ | |||||
| RemarkGfm, | |||||
| [RemarkMath, { singleDollarTextMath: false }], | |||||
| RemarkBreaks, | |||||
| ]} | |||||
| rehypePlugins={[ | |||||
| RehypeKatex, | |||||
| RehypeRaw as any, | |||||
| // The Rehype plug-in is used to remove the ref attribute of an element | |||||
| () => { | |||||
| return (tree: any) => { | |||||
| const iterate = (node: any) => { | |||||
| if (node.type === 'element' && node.properties?.ref) | |||||
| delete node.properties.ref | |||||
| if (node.type === 'element' && !/^[a-z][a-z0-9]*$/i.test(node.tagName)) { | |||||
| node.type = 'text' | |||||
| node.value = `<${node.tagName}` | |||||
| } | |||||
| if (node.children) | |||||
| node.children.forEach(iterate) | |||||
| } | |||||
| tree.children.forEach(iterate) | |||||
| } | |||||
| }, | |||||
| ]} | |||||
| disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]} | |||||
| components={{ | |||||
| code: CodeBlock, | |||||
| img: Img, | |||||
| video: VideoBlock, | |||||
| audio: AudioBlock, | |||||
| a: Link, | |||||
| p: Paragraph, | |||||
| button: MarkdownButton, | |||||
| form: MarkdownForm, | |||||
| script: ScriptBlock as any, | |||||
| details: ThinkBlock, | |||||
| }} | |||||
| > | |||||
| {/* Markdown detect has problem. */} | |||||
| {latexContent} | |||||
| </ReactMarkdown> | |||||
| </div> | |||||
| ) | |||||
| } |
| /** | |||||
| * @fileoverview Utility functions for preprocessing Markdown content. | |||||
| * These functions were extracted from the main markdown renderer for better separation of concerns. | |||||
| * Includes preprocessing for LaTeX and custom "think" tags. | |||||
| */ | |||||
| import { flow } from 'lodash-es' | |||||
| export const preprocessLaTeX = (content: string) => { | |||||
| if (typeof content !== 'string') | |||||
| return content | |||||
| const codeBlockRegex = /```[\s\S]*?```/g | |||||
| const codeBlocks = content.match(codeBlockRegex) || [] | |||||
| let processedContent = content.replace(codeBlockRegex, 'CODE_BLOCK_PLACEHOLDER') | |||||
| processedContent = flow([ | |||||
| (str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`), | |||||
| (str: string) => str.replace(/\\\[([\s\S]*?)\\\]/g, (_, equation) => `$$${equation}$$`), | |||||
| (str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`), | |||||
| (str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`), | |||||
| ])(processedContent) | |||||
| codeBlocks.forEach((block) => { | |||||
| processedContent = processedContent.replace('CODE_BLOCK_PLACEHOLDER', block) | |||||
| }) | |||||
| return processedContent | |||||
| } | |||||
| export const preprocessThinkTag = (content: string) => { | |||||
| const thinkOpenTagRegex = /<think>\n/g | |||||
| const thinkCloseTagRegex = /\n<\/think>/g | |||||
| return flow([ | |||||
| (str: string) => str.replace(thinkOpenTagRegex, '<details data-think=true>\n'), | |||||
| (str: string) => str.replace(thinkCloseTagRegex, '\n[ENDTHINKFLAG]</details>'), | |||||
| ])(content) | |||||
| } |