File size: 3,363 Bytes
9705b6c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
import React, { useState, useEffect } from 'react';
import type { TMessage } from 'librechat-data-provider';
import { useRecoilValue } from 'recoil';
import ReactMarkdown from 'react-markdown';
import type { PluggableList } from 'unified';
import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import remarkMath from 'remark-math';
import supersub from 'remark-supersub';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import CodeBlock from './CodeBlock';
import { langSubset, validateIframe } from '~/utils';
import store from '~/store';
type TCodeProps = {
inline: boolean;
className: string;
children: React.ReactNode;
};
type TContentProps = {
content: string;
message: TMessage;
showCursor?: boolean;
};
const code = React.memo(({ inline, className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className || '');
const lang = match && match[1];
if (inline) {
return <code className={className}>{children}</code>;
} else {
return <CodeBlock lang={lang || 'text'} codeChildren={children} />;
}
});
const p = React.memo(({ children }: { children: React.ReactNode }) => {
return <p className="mb-2 whitespace-pre-wrap">{children}</p>;
});
const Markdown = React.memo(({ content, message, showCursor }: TContentProps) => {
const [cursor, setCursor] = useState('β');
const isSubmitting = useRecoilValue(store.isSubmitting);
const latestMessage = useRecoilValue(store.latestMessage);
const isInitializing = content === '<span className="result-streaming">β</span>';
const { isEdited, messageId } = message ?? {};
const isLatestMessage = messageId === latestMessage?.messageId;
const currentContent = content?.replace('z-index: 1;', '') ?? '';
useEffect(() => {
let timer1: NodeJS.Timeout, timer2: NodeJS.Timeout;
if (!showCursor) {
setCursor('γ
€');
return;
}
if (isSubmitting && isLatestMessage) {
timer1 = setInterval(() => {
setCursor('γ
€');
timer2 = setTimeout(() => {
setCursor('β');
}, 200);
}, 1000);
} else {
setCursor('γ
€');
}
// This is the cleanup function that React will run when the component unmounts
return () => {
clearInterval(timer1);
clearTimeout(timer2);
};
}, [isSubmitting, isLatestMessage, showCursor]);
const rehypePlugins: PluggableList = [
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: langSubset,
},
],
[rehypeRaw],
];
let isValidIframe: string | boolean | null = false;
if (!isEdited) {
isValidIframe = validateIframe(currentContent);
}
if (isEdited || ((!isInitializing || !isLatestMessage) && !isValidIframe)) {
rehypePlugins.pop();
}
return (
<ReactMarkdown
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
rehypePlugins={rehypePlugins}
linkTarget="_new"
components={
{
code,
p,
} as {
[nodeType: string]: React.ElementType;
}
}
>
{isLatestMessage && isSubmitting && !isInitializing
? currentContent + cursor
: currentContent}
</ReactMarkdown>
);
});
export default Markdown;
|