* feat: markdown extension * media cros * rerank test * default price * perf: default model * fix: cannot custom provider * fix: default model select * update bg * perf: default model selector * fix: usage export * i18n * fix: rerank * update init extension * perf: ip limit check * doubao model order * web default modle * perf: tts selector * perf: tts error * qrcode package
176 lines
5.7 KiB
TypeScript
176 lines
5.7 KiB
TypeScript
import React, { useCallback, useMemo } from 'react';
|
||
import ReactMarkdown from 'react-markdown';
|
||
import 'katex/dist/katex.min.css';
|
||
import RemarkMath from 'remark-math'; // Math syntax
|
||
import RemarkBreaks from 'remark-breaks'; // Line break
|
||
import RehypeKatex from 'rehype-katex'; // Math render
|
||
import RemarkGfm from 'remark-gfm'; // Special markdown syntax
|
||
import RehypeExternalLinks from 'rehype-external-links';
|
||
|
||
import styles from './index.module.scss';
|
||
import dynamic from 'next/dynamic';
|
||
|
||
import { Box } from '@chakra-ui/react';
|
||
import { CodeClassNameEnum } from './utils';
|
||
|
||
const CodeLight = dynamic(() => import('./codeBlock/CodeLight'), { ssr: false });
|
||
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false });
|
||
const MdImage = dynamic(() => import('./img/Image'), { ssr: false });
|
||
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'), { ssr: false });
|
||
const IframeCodeBlock = dynamic(() => import('./codeBlock/Iframe'), { ssr: false });
|
||
const IframeHtmlCodeBlock = dynamic(() => import('./codeBlock/iframe-html'), { ssr: false });
|
||
const VideoBlock = dynamic(() => import('./codeBlock/Video'), { ssr: false });
|
||
const AudioBlock = dynamic(() => import('./codeBlock/Audio'), { ssr: false });
|
||
|
||
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
|
||
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
|
||
const A = dynamic(() => import('./A'), { ssr: false });
|
||
|
||
type Props = {
|
||
source?: string;
|
||
showAnimation?: boolean;
|
||
isDisabled?: boolean;
|
||
forbidZhFormat?: boolean;
|
||
};
|
||
const Markdown = (props: Props) => {
|
||
const source = props.source || '';
|
||
|
||
if (source.length < 200000) {
|
||
return <MarkdownRender {...props} />;
|
||
}
|
||
|
||
return <Box whiteSpace={'pre-wrap'}>{source}</Box>;
|
||
};
|
||
const MarkdownRender = ({ source = '', showAnimation, isDisabled, forbidZhFormat }: Props) => {
|
||
const components = useMemo<any>(
|
||
() => ({
|
||
img: Image,
|
||
pre: RewritePre,
|
||
code: Code,
|
||
a: A
|
||
}),
|
||
[]
|
||
);
|
||
|
||
const formatSource = useMemo(() => {
|
||
if (showAnimation || forbidZhFormat) return source;
|
||
|
||
// 保护 URL 格式:https://, http://, /api/xxx
|
||
const urlPlaceholders: string[] = [];
|
||
const textWithProtectedUrls = source.replace(
|
||
/(https?:\/\/[^\s<]+[^<.,:;"')\]\s]|\/api\/[^\s]+)(?=\s|$)/g,
|
||
(match) => {
|
||
urlPlaceholders.push(match);
|
||
return `__URL_${urlPlaceholders.length - 1}__`;
|
||
}
|
||
);
|
||
|
||
// 处理中文与英文数字之间的分词
|
||
const textWithSpaces = textWithProtectedUrls
|
||
.replace(
|
||
/([\u4e00-\u9fa5\u3000-\u303f])([a-zA-Z0-9])|([a-zA-Z0-9])([\u4e00-\u9fa5\u3000-\u303f])/g,
|
||
'$1$3 $2$4'
|
||
)
|
||
// 处理引用标记
|
||
.replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1')
|
||
// 处理 [quote:id] 格式引用,将 [quote:675934a198f46329dfc6d05a] 转换为 [675934a198f46329dfc6d05a]()
|
||
.replace(/\[quote:?\s*([a-f0-9]{24})\](?!\()/gi, '[$1](QUOTE)')
|
||
.replace(/\[([a-f0-9]{24})\](?!\()/g, '[$1](QUOTE)');
|
||
|
||
// 还原 URL
|
||
const finalText = textWithSpaces.replace(
|
||
/__URL_(\d+)__/g,
|
||
(_, index) => urlPlaceholders[parseInt(index)]
|
||
);
|
||
|
||
return finalText;
|
||
}, [forbidZhFormat, showAnimation, source]);
|
||
|
||
const urlTransform = useCallback((val: string) => {
|
||
return val;
|
||
}, []);
|
||
|
||
return (
|
||
<Box position={'relative'}>
|
||
<ReactMarkdown
|
||
className={`markdown ${styles.markdown}
|
||
${showAnimation ? `${formatSource ? styles.waitingAnimation : styles.animation}` : ''}
|
||
`}
|
||
remarkPlugins={[RemarkMath, [RemarkGfm, { singleTilde: false }], RemarkBreaks]}
|
||
rehypePlugins={[RehypeKatex, [RehypeExternalLinks, { target: '_blank' }]]}
|
||
components={components}
|
||
urlTransform={urlTransform}
|
||
>
|
||
{formatSource}
|
||
</ReactMarkdown>
|
||
{isDisabled && <Box position={'absolute'} top={0} right={0} left={0} bottom={0} />}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default React.memo(Markdown);
|
||
|
||
/* Custom dom */
|
||
function Code(e: any) {
|
||
const { className, codeBlock, children } = e;
|
||
const match = /language-(\w+)/.exec(className || '');
|
||
const codeType = match?.[1];
|
||
|
||
const strChildren = String(children);
|
||
|
||
const Component = useMemo(() => {
|
||
if (codeType === CodeClassNameEnum.mermaid) {
|
||
return <MermaidCodeBlock code={strChildren} />;
|
||
}
|
||
if (codeType === CodeClassNameEnum.guide) {
|
||
return <ChatGuide text={strChildren} />;
|
||
}
|
||
if (codeType === CodeClassNameEnum.questionGuide) {
|
||
return <QuestionGuide text={strChildren} />;
|
||
}
|
||
if (codeType === CodeClassNameEnum.echarts) {
|
||
return <EChartsCodeBlock code={strChildren} />;
|
||
}
|
||
if (codeType === CodeClassNameEnum.iframe) {
|
||
return <IframeCodeBlock code={strChildren} />;
|
||
}
|
||
if (codeType && codeType.toLowerCase() === CodeClassNameEnum.html) {
|
||
return (
|
||
<IframeHtmlCodeBlock className={className} codeBlock={codeBlock} match={match}>
|
||
{children}
|
||
</IframeHtmlCodeBlock>
|
||
);
|
||
}
|
||
if (codeType === CodeClassNameEnum.video) {
|
||
return <VideoBlock code={strChildren} />;
|
||
}
|
||
if (codeType === CodeClassNameEnum.audio) {
|
||
return <AudioBlock code={strChildren} />;
|
||
}
|
||
|
||
return (
|
||
<CodeLight className={className} codeBlock={codeBlock} match={match}>
|
||
{children}
|
||
</CodeLight>
|
||
);
|
||
}, [codeType, className, codeBlock, match, children, strChildren]);
|
||
|
||
return Component;
|
||
}
|
||
|
||
function Image({ src }: { src?: string }) {
|
||
return <MdImage src={src} />;
|
||
}
|
||
|
||
function RewritePre({ children }: any) {
|
||
const modifiedChildren = React.Children.map(children, (child) => {
|
||
if (React.isValidElement(child)) {
|
||
// @ts-ignore
|
||
return React.cloneElement(child, { codeBlock: true });
|
||
}
|
||
return child;
|
||
});
|
||
|
||
return <>{modifiedChildren}</>;
|
||
}
|