mirror of
https://github.com/labring/FastGPT.git
synced 2025-08-03 13:38:00 +00:00
monorepo packages (#344)
This commit is contained in:
327
projects/app/src/components/Markdown/CodeLight.tsx
Normal file
327
projects/app/src/components/Markdown/CodeLight.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import React from 'react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||
import Icon from '@/components/Icon';
|
||||
import { useCopyData } from '@/hooks/useCopyData';
|
||||
|
||||
const codeLight: { [key: string]: React.CSSProperties } = {
|
||||
'code[class*=language-]': {
|
||||
color: '#d4d4d4',
|
||||
textShadow: 'none',
|
||||
direction: 'ltr',
|
||||
textAlign: 'left',
|
||||
whiteSpace: 'pre',
|
||||
wordSpacing: 'normal',
|
||||
wordBreak: 'normal',
|
||||
lineHeight: '1.5',
|
||||
MozTabSize: '4',
|
||||
OTabSize: '4',
|
||||
tabSize: '4',
|
||||
WebkitHyphens: 'none',
|
||||
MozHyphens: 'none',
|
||||
msHyphens: 'none',
|
||||
hyphens: 'none'
|
||||
},
|
||||
'pre[class*=language-]': {
|
||||
color: '#d4d4d4',
|
||||
textShadow: 'none',
|
||||
direction: 'ltr',
|
||||
textAlign: 'left',
|
||||
whiteSpace: 'pre',
|
||||
wordSpacing: 'normal',
|
||||
wordBreak: 'normal',
|
||||
lineHeight: '1.5',
|
||||
MozTabSize: '4',
|
||||
OTabSize: '4',
|
||||
tabSize: '4',
|
||||
WebkitHyphens: 'none',
|
||||
MozHyphens: 'none',
|
||||
msHyphens: 'none',
|
||||
hyphens: 'none',
|
||||
padding: '1em',
|
||||
margin: '.5em 0',
|
||||
overflow: 'auto',
|
||||
background: '#1e1e1e'
|
||||
},
|
||||
'code[class*=language-] ::selection': {
|
||||
textShadow: 'none',
|
||||
background: '#264f78'
|
||||
},
|
||||
'code[class*=language-]::selection': {
|
||||
textShadow: 'none',
|
||||
background: '#264f78'
|
||||
},
|
||||
'pre[class*=language-] ::selection': {
|
||||
textShadow: 'none',
|
||||
background: '#264f78'
|
||||
},
|
||||
'pre[class*=language-]::selection': {
|
||||
textShadow: 'none',
|
||||
background: '#264f78'
|
||||
},
|
||||
':not(pre)>code[class*=language-]': {
|
||||
padding: '.1em .3em',
|
||||
borderRadius: '.3em',
|
||||
color: '#db4c69',
|
||||
background: '#1e1e1e'
|
||||
},
|
||||
'.namespace': {
|
||||
opacity: '0.7'
|
||||
},
|
||||
'doctype.doctype-tag': {
|
||||
color: '#569cd6'
|
||||
},
|
||||
'doctype.name': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
comment: {
|
||||
color: '#6a9955'
|
||||
},
|
||||
prolog: {
|
||||
color: '#6a9955'
|
||||
},
|
||||
'.language-html .language-css .token.punctuation': {
|
||||
color: '#d4d4d4'
|
||||
},
|
||||
'.language-html .language-javascript .token.punctuation': {
|
||||
color: '#d4d4d4'
|
||||
},
|
||||
punctuation: {
|
||||
color: '#d4d4d4'
|
||||
},
|
||||
boolean: {
|
||||
color: '#569cd6'
|
||||
},
|
||||
constant: {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
inserted: {
|
||||
color: '#b5cea8'
|
||||
},
|
||||
number: {
|
||||
color: '#b5cea8'
|
||||
},
|
||||
property: {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
symbol: {
|
||||
color: '#b5cea8'
|
||||
},
|
||||
tag: {
|
||||
color: '#569cd6'
|
||||
},
|
||||
unit: {
|
||||
color: '#b5cea8'
|
||||
},
|
||||
'attr-name': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
builtin: {
|
||||
color: '#ce9178'
|
||||
},
|
||||
char: {
|
||||
color: '#ce9178'
|
||||
},
|
||||
deleted: {
|
||||
color: '#ce9178'
|
||||
},
|
||||
selector: {
|
||||
color: '#d7ba7d'
|
||||
},
|
||||
string: {
|
||||
color: '#ce9178'
|
||||
},
|
||||
'.language-css .token.string.url': {
|
||||
textDecoration: 'underline'
|
||||
},
|
||||
entity: {
|
||||
color: '#569cd6'
|
||||
},
|
||||
operator: {
|
||||
color: '#d4d4d4'
|
||||
},
|
||||
'operator.arrow': {
|
||||
color: '#569cd6'
|
||||
},
|
||||
atrule: {
|
||||
color: '#ce9178'
|
||||
},
|
||||
'atrule.rule': {
|
||||
color: '#c586c0'
|
||||
},
|
||||
'atrule.url': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'atrule.url.function': {
|
||||
color: '#dcdcaa'
|
||||
},
|
||||
'atrule.url.punctuation': {
|
||||
color: '#d4d4d4'
|
||||
},
|
||||
keyword: {
|
||||
color: '#569cd6'
|
||||
},
|
||||
'keyword.control-flow': {
|
||||
color: '#c586c0'
|
||||
},
|
||||
'keyword.module': {
|
||||
color: '#c586c0'
|
||||
},
|
||||
function: {
|
||||
color: '#dcdcaa'
|
||||
},
|
||||
'function.maybe-class-name': {
|
||||
color: '#dcdcaa'
|
||||
},
|
||||
regex: {
|
||||
color: '#d16969'
|
||||
},
|
||||
important: {
|
||||
color: '#569cd6'
|
||||
},
|
||||
italic: {
|
||||
fontStyle: 'italic'
|
||||
},
|
||||
'class-name': {
|
||||
color: '#4ec9b0'
|
||||
},
|
||||
'maybe-class-name': {
|
||||
color: '#4ec9b0'
|
||||
},
|
||||
console: {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
parameter: {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
interpolation: {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'punctuation.interpolation-punctuation': {
|
||||
color: '#569cd6'
|
||||
},
|
||||
'exports.maybe-class-name': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'imports.maybe-class-name': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
variable: {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
escape: {
|
||||
color: '#d7ba7d'
|
||||
},
|
||||
'tag.punctuation': {
|
||||
color: 'grey'
|
||||
},
|
||||
cdata: {
|
||||
color: 'grey'
|
||||
},
|
||||
'attr-value': {
|
||||
color: '#ce9178'
|
||||
},
|
||||
'attr-value.punctuation': {
|
||||
color: '#ce9178'
|
||||
},
|
||||
'attr-value.punctuation.attr-equals': {
|
||||
color: '#d4d4d4'
|
||||
},
|
||||
namespace: {
|
||||
color: '#4ec9b0'
|
||||
},
|
||||
'code[class*=language-javascript]': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'code[class*=language-jsx]': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'code[class*=language-tsx]': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'code[class*=language-typescript]': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'pre[class*=language-javascript]': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'pre[class*=language-jsx]': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'pre[class*=language-tsx]': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'pre[class*=language-typescript]': {
|
||||
color: '#9cdcfe'
|
||||
},
|
||||
'code[class*=language-css]': {
|
||||
color: '#ce9178'
|
||||
},
|
||||
'pre[class*=language-css]': {
|
||||
color: '#ce9178'
|
||||
},
|
||||
'code[class*=language-html]': {
|
||||
color: '#d4d4d4'
|
||||
},
|
||||
'pre[class*=language-html]': {
|
||||
color: '#d4d4d4'
|
||||
},
|
||||
'.language-regex .token.anchor': {
|
||||
color: '#dcdcaa'
|
||||
},
|
||||
'.language-html .token.punctuation': {
|
||||
color: 'grey'
|
||||
},
|
||||
'pre[class*=language-]>code[class*=language-]': {
|
||||
position: 'relative',
|
||||
zIndex: '1'
|
||||
},
|
||||
'.line-highlight.line-highlight': {
|
||||
background: '#f7ebc6',
|
||||
boxShadow: 'inset 5px 0 0 #f7d87c',
|
||||
zIndex: '0'
|
||||
}
|
||||
};
|
||||
|
||||
const CodeLight = ({
|
||||
children,
|
||||
className,
|
||||
inline,
|
||||
match
|
||||
}: {
|
||||
children: React.ReactNode & React.ReactNode[];
|
||||
className?: string;
|
||||
inline?: boolean;
|
||||
match: RegExpExecArray | null;
|
||||
}) => {
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
if (!inline && match) {
|
||||
return (
|
||||
<Box my={3} borderRadius={'md'} overflow={'overlay'} backgroundColor={'#222'}>
|
||||
<Flex
|
||||
className="code-header"
|
||||
py={2}
|
||||
px={5}
|
||||
backgroundColor={useColorModeValue('#323641', 'gray.600')}
|
||||
color={'#fff'}
|
||||
fontSize={'sm'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Box flex={1}>{match?.[1]}</Box>
|
||||
<Flex cursor={'pointer'} onClick={() => copyData(String(children))} alignItems={'center'}>
|
||||
<Icon name={'copy'} width={15} height={15} fill={'#fff'}></Icon>
|
||||
<Box ml={1}>复制</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre">
|
||||
{String(children)}
|
||||
</SyntaxHighlighter>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <code className={className}>{children}</code>;
|
||||
};
|
||||
|
||||
export default React.memo(CodeLight);
|
58
projects/app/src/components/Markdown/chat/Guide.tsx
Normal file
58
projects/app/src/components/Markdown/chat/Guide.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Link } from '@chakra-ui/react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
import RemarkMath from 'remark-math';
|
||||
import RehypeKatex from 'rehype-katex';
|
||||
import { event } from '@/utils/plugin/eventbus';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import styles from '../index.module.scss';
|
||||
import Image from '../img/Image';
|
||||
|
||||
function MyLink(e: any) {
|
||||
const href = e.href;
|
||||
const text = String(e.children);
|
||||
|
||||
return !!href ? (
|
||||
<Link href={href} target={'_blank'}>
|
||||
{text}
|
||||
</Link>
|
||||
) : (
|
||||
<Box as={'ul'}>
|
||||
<Box as={'li'}>
|
||||
<Box
|
||||
as={'span'}
|
||||
color={'blue.600'}
|
||||
textDecoration={'underline'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
event.emit('guideClick', { text });
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const Guide = ({ text }: { text: string }) => {
|
||||
const formatText = useMemo(() => text.replace(/\[(.*?)\]($|\n)/g, '[$1]()\n'), [text]);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}`}
|
||||
remarkPlugins={[RemarkGfm, RemarkMath]}
|
||||
rehypePlugins={[RehypeKatex]}
|
||||
components={{
|
||||
a: MyLink,
|
||||
img: Image
|
||||
}}
|
||||
>
|
||||
{formatText}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Guide);
|
64
projects/app/src/components/Markdown/chat/Quote.tsx
Normal file
64
projects/app/src/components/Markdown/chat/Quote.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
import { getFileAndOpen } from '@/utils/web/file';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
|
||||
type QuoteItemType = {
|
||||
file_id?: string;
|
||||
filename: string;
|
||||
};
|
||||
|
||||
const QuoteBlock = ({ code }: { code: string }) => {
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const quoteList = useMemo(() => {
|
||||
try {
|
||||
return JSON.parse(code) as QuoteItemType[];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
return (
|
||||
<Box mt={3} pt={2} borderTop={theme.borders.base}>
|
||||
{quoteList.length > 0 ? (
|
||||
<>
|
||||
<Box>本次回答的引用:</Box>
|
||||
<Box as={'ol'}>
|
||||
{quoteList.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
as={'li'}
|
||||
{...(item.file_id
|
||||
? {
|
||||
textDecoration: 'underline',
|
||||
color: 'myBlue.800',
|
||||
cursor: 'pointer'
|
||||
}
|
||||
: {})}
|
||||
onClick={async () => {
|
||||
if (!item.file_id) return;
|
||||
try {
|
||||
await getFileAndOpen(item.file_id);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error, '打开文件失败')
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.filename}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box>正在生成引用……</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuoteBlock;
|
@@ -0,0 +1,64 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import { Box, Skeleton } from '@chakra-ui/react';
|
||||
|
||||
const EChartsCodeBlock = ({ code }: { code: string }) => {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
const eChart = useRef<ECharts>();
|
||||
const [option, setOption] = useState<any>();
|
||||
const [width, setWidth] = useState(400);
|
||||
|
||||
useEffect(() => {
|
||||
const clientWidth = document.getElementById('chat-container')?.clientWidth || 500;
|
||||
setWidth(clientWidth * 0.9);
|
||||
setTimeout(() => {
|
||||
eChart.current?.resize();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let option;
|
||||
try {
|
||||
option = JSON.parse(code.trim());
|
||||
option = {
|
||||
...option,
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
setOption(option);
|
||||
} catch (error) {}
|
||||
|
||||
if (!option) return;
|
||||
|
||||
(async () => {
|
||||
// @ts-ignore
|
||||
await import('echarts-gl');
|
||||
})();
|
||||
|
||||
if (chartRef.current) {
|
||||
eChart.current = echarts.init(chartRef.current);
|
||||
eChart.current.setOption(option);
|
||||
eChart.current?.resize();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (eChart.current) {
|
||||
eChart.current.dispose();
|
||||
}
|
||||
};
|
||||
}, [code]);
|
||||
|
||||
return (
|
||||
<Box overflowX={'auto'}>
|
||||
<Box h={'400px'} minW={'400px'} w={`${width}px`} ref={chartRef} />
|
||||
{!option && <Skeleton isLoaded={true} fadeDuration={2} h={'400px'} w={`400px`}></Skeleton>}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EChartsCodeBlock;
|
40
projects/app/src/components/Markdown/img/Image.tsx
Normal file
40
projects/app/src/components/Markdown/img/Image.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Image, Skeleton } from '@chakra-ui/react';
|
||||
|
||||
const MdImage = ({ src }: { src?: string }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [succeed, setSucceed] = useState(false);
|
||||
return (
|
||||
<Skeleton
|
||||
minH="100px"
|
||||
isLoaded={!isLoading}
|
||||
fadeDuration={2}
|
||||
display={'flex'}
|
||||
justifyContent={'center'}
|
||||
my={1}
|
||||
>
|
||||
<Image
|
||||
display={'inline-block'}
|
||||
borderRadius={'md'}
|
||||
src={src}
|
||||
alt={''}
|
||||
fallbackSrc={'/imgs/errImg.png'}
|
||||
fallbackStrategy={'onError'}
|
||||
cursor={succeed ? 'pointer' : 'default'}
|
||||
loading="eager"
|
||||
objectFit={'contain'}
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
setSucceed(true);
|
||||
}}
|
||||
onError={() => setIsLoading(false)}
|
||||
onClick={() => {
|
||||
if (!succeed) return;
|
||||
window.open(src, '_blank');
|
||||
}}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(MdImage);
|
137
projects/app/src/components/Markdown/img/MermaidCodeBlock.tsx
Normal file
137
projects/app/src/components/Markdown/img/MermaidCodeBlock.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React, { useEffect, useRef, memo, useCallback, useState, useMemo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
// @ts-ignore
|
||||
import mermaid from 'mermaid';
|
||||
import MyIcon from '../../Icon';
|
||||
|
||||
const mermaidAPI = mermaid.mermaidAPI;
|
||||
mermaidAPI.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'base',
|
||||
flowchart: {
|
||||
useMaxWidth: false
|
||||
},
|
||||
themeVariables: {
|
||||
fontSize: '14px',
|
||||
primaryColor: '#d6e8ff',
|
||||
primaryTextColor: '#485058',
|
||||
primaryBorderColor: '#fff',
|
||||
lineColor: '#5A646E',
|
||||
secondaryColor: '#B5E9E5',
|
||||
tertiaryColor: '#485058'
|
||||
}
|
||||
});
|
||||
|
||||
const punctuationMap: Record<string, string> = {
|
||||
',': ',',
|
||||
';': ';',
|
||||
'。': '.',
|
||||
':': ':',
|
||||
'!': '!',
|
||||
'?': '?',
|
||||
'“': '"',
|
||||
'”': '"',
|
||||
'‘': "'",
|
||||
'’': "'",
|
||||
'【': '[',
|
||||
'】': ']',
|
||||
'(': '(',
|
||||
')': ')',
|
||||
'《': '<',
|
||||
'》': '>',
|
||||
'、': ','
|
||||
};
|
||||
|
||||
const MermaidBlock = ({ code }: { code: string }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [svg, setSvg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!code) return;
|
||||
try {
|
||||
const formatCode = code.replace(
|
||||
new RegExp(`[${Object.keys(punctuationMap).join('')}]`, 'g'),
|
||||
(match) => punctuationMap[match]
|
||||
);
|
||||
const { svg } = await mermaid.render(`mermaid-${Date.now()}`, formatCode);
|
||||
setSvg(svg);
|
||||
} catch (e: any) {
|
||||
// console.log('[Mermaid] ', e?.message);
|
||||
}
|
||||
})();
|
||||
}, [code]);
|
||||
|
||||
const onclickExport = useCallback(() => {
|
||||
const svg = ref.current?.children[0];
|
||||
if (!svg) return;
|
||||
|
||||
const rate = svg.clientHeight / svg.clientWidth;
|
||||
const w = 3000;
|
||||
const h = rate * w;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
// 绘制白色背景
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
const img = new Image();
|
||||
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(ref.current?.innerHTML)}`;
|
||||
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
const jpgDataUrl = canvas.toDataURL('image/jpeg', 1);
|
||||
const a = document.createElement('a');
|
||||
a.href = jpgDataUrl;
|
||||
a.download = 'mermaid.jpg';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
img.onerror = (e) => {
|
||||
console.log(e);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
position={'relative'}
|
||||
_hover={{
|
||||
'& > .export': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
overflowX={'auto'}
|
||||
ref={ref}
|
||||
minW={'100px'}
|
||||
minH={'50px'}
|
||||
py={4}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
<MyIcon
|
||||
className="export"
|
||||
display={'none'}
|
||||
name={'export'}
|
||||
w={'20px'}
|
||||
position={'absolute'}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
color: 'myBlue.700'
|
||||
}}
|
||||
right={0}
|
||||
top={0}
|
||||
cursor={'pointer'}
|
||||
onClick={onclickExport}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MermaidBlock;
|
420
projects/app/src/components/Markdown/index.module.scss
Normal file
420
projects/app/src/components/Markdown/index.module.scss
Normal file
@@ -0,0 +1,420 @@
|
||||
.waitingAnimation::after {
|
||||
display: inline-block;
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
transform: translate(4px, 2px) scaleY(1.3);
|
||||
background-color: var(--chakra-colors-chakra-body-text);
|
||||
animation: blink 0.6s infinite;
|
||||
}
|
||||
.animation {
|
||||
> :last-child::after {
|
||||
display: inline-block;
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
transform: translate(4px, 2px) scaleY(1.3);
|
||||
background-color: var(--chakra-colors-chakra-body-text);
|
||||
animation: blink 0.6s infinite;
|
||||
}
|
||||
}
|
||||
@keyframes blink {
|
||||
from,
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown > *:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.markdown > *:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.markdown a.absent {
|
||||
color: #cc0000;
|
||||
}
|
||||
.markdown a.anchor {
|
||||
bottom: 0;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
left: 0;
|
||||
margin-left: -30px;
|
||||
padding-left: 30px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.markdown h1,
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4,
|
||||
.markdown h5,
|
||||
.markdown h6 {
|
||||
cursor: text;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
.markdown h1 .mini-icon-link,
|
||||
.markdown h2 .mini-icon-link,
|
||||
.markdown h3 .mini-icon-link,
|
||||
.markdown h4 .mini-icon-link,
|
||||
.markdown h5 .mini-icon-link,
|
||||
.markdown h6 .mini-icon-link {
|
||||
display: none;
|
||||
}
|
||||
.markdown h1:hover a.anchor,
|
||||
.markdown h2:hover a.anchor,
|
||||
.markdown h3:hover a.anchor,
|
||||
.markdown h4:hover a.anchor,
|
||||
.markdown h5:hover a.anchor,
|
||||
.markdown h6:hover a.anchor {
|
||||
line-height: 1;
|
||||
margin-left: -22px;
|
||||
padding-left: 0;
|
||||
text-decoration: none;
|
||||
top: 15%;
|
||||
}
|
||||
.markdown h1:hover a.anchor .mini-icon-link,
|
||||
.markdown h2:hover a.anchor .mini-icon-link,
|
||||
.markdown h3:hover a.anchor .mini-icon-link,
|
||||
.markdown h4:hover a.anchor .mini-icon-link,
|
||||
.markdown h5:hover a.anchor .mini-icon-link,
|
||||
.markdown h6:hover a.anchor .mini-icon-link {
|
||||
display: inline-block;
|
||||
}
|
||||
.markdown h1 tt,
|
||||
.markdown h1 code,
|
||||
.markdown h2 tt,
|
||||
.markdown h2 code,
|
||||
.markdown h3 tt,
|
||||
.markdown h3 code,
|
||||
.markdown h4 tt,
|
||||
.markdown h4 code,
|
||||
.markdown h5 tt,
|
||||
.markdown h5 code,
|
||||
.markdown h6 tt,
|
||||
.markdown h6 code {
|
||||
font-size: inherit;
|
||||
}
|
||||
.markdown h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
.markdown h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.markdown h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.markdown h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
.markdown h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
.markdown h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
.markdown p,
|
||||
.markdown blockquote,
|
||||
.markdown ul,
|
||||
.markdown ol,
|
||||
.markdown dl,
|
||||
.markdown table,
|
||||
.markdown pre {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.markdown > h2:first-child,
|
||||
.markdown > h1:first-child,
|
||||
.markdown > h1:first-child + h2,
|
||||
.markdown > h3:first-child,
|
||||
.markdown > h4:first-child,
|
||||
.markdown > h5:first-child,
|
||||
.markdown > h6:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
.markdown a:first-child h1,
|
||||
.markdown a:first-child h2,
|
||||
.markdown a:first-child h3,
|
||||
.markdown a:first-child h4,
|
||||
.markdown a:first-child h5,
|
||||
.markdown a:first-child h6 {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
.markdown h1 + p,
|
||||
.markdown h2 + p,
|
||||
.markdown h3 + p,
|
||||
.markdown h4 + p,
|
||||
.markdown h5 + p,
|
||||
.markdown h6 + p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown li p.first {
|
||||
display: inline-block;
|
||||
}
|
||||
.markdown ul,
|
||||
.markdown ol {
|
||||
padding-left: 2em;
|
||||
}
|
||||
.markdown ul.no-list,
|
||||
.markdown ol.no-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown ul li > *:first-child,
|
||||
.markdown ol li > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown ul ul,
|
||||
.markdown ul ol,
|
||||
.markdown ol ol,
|
||||
.markdown ol ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown dl {
|
||||
padding: 0;
|
||||
}
|
||||
.markdown dl dt {
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
margin: 15px 0 5px;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown dl dt:first-child {
|
||||
padding: 0;
|
||||
}
|
||||
.markdown dl dt > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown dl dt > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown dl dd {
|
||||
margin: 0 0 15px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.markdown dl dd > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown dl dd > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown blockquote {
|
||||
border-left: 4px solid #dddddd;
|
||||
color: #777777;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.markdown blockquote > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown blockquote > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown table th {
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown table th,
|
||||
.markdown table td {
|
||||
padding: 6px 13px;
|
||||
}
|
||||
.markdown table tr {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.markdown table tr:nth-child(2n) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.markdown img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.markdown span.frame {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.markdown span.frame > span {
|
||||
border: 1px solid #dddddd;
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 13px 0 0;
|
||||
overflow: hidden;
|
||||
padding: 7px;
|
||||
width: auto;
|
||||
}
|
||||
.markdown span.frame span img {
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
.markdown span.frame span span {
|
||||
clear: both;
|
||||
color: #333333;
|
||||
display: block;
|
||||
padding: 5px 0 0;
|
||||
}
|
||||
.markdown span.align-center {
|
||||
clear: both;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.markdown span.align-center > span {
|
||||
display: block;
|
||||
margin: 13px auto 0;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
.markdown span.align-center span img {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.markdown span.align-right {
|
||||
clear: both;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.markdown span.align-right > span {
|
||||
display: block;
|
||||
margin: 13px 0 0;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
}
|
||||
.markdown span.align-right span img {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
.markdown span.float-left {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.markdown span.float-left span {
|
||||
margin: 13px 0 0;
|
||||
}
|
||||
.markdown span.float-right {
|
||||
display: block;
|
||||
float: right;
|
||||
margin-left: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.markdown span.float-right > span {
|
||||
display: block;
|
||||
margin: 13px auto 0;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
}
|
||||
.markdown code,
|
||||
.markdown tt {
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
margin: 0 2px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.markdown pre > code {
|
||||
background: none repeat scroll 0 0 transparent;
|
||||
border: medium none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown .highlight pre,
|
||||
.markdown pre {
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
font-size: max(0.9em, 14px);
|
||||
line-height: 19px;
|
||||
overflow: auto;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
.markdown pre code,
|
||||
.markdown pre tt {
|
||||
background-color: transparent;
|
||||
border: medium none;
|
||||
}
|
||||
.markdown hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.markdown {
|
||||
text-align: justify;
|
||||
tab-size: 4;
|
||||
word-spacing: normal;
|
||||
width: 100%;
|
||||
|
||||
* {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: #292b33 !important;
|
||||
overflow-x: auto;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: #292b33 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--chakra-colors-blue-600);
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0px;
|
||||
color: var(--chakra-colors-gray-700);
|
||||
|
||||
thead tr:first-child th {
|
||||
border-bottom-width: 1px;
|
||||
border-left-width: 1px;
|
||||
border-top-width: 1px;
|
||||
border-color: #ccc;
|
||||
background-color: rgba(236, 236, 241, 0.2);
|
||||
overflow: hidden;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.375rem;
|
||||
}
|
||||
&:last-child {
|
||||
border-right-width: 1px;
|
||||
border-top-right-radius: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom-width: 1px;
|
||||
border-left-width: 1px;
|
||||
border-color: #ccc;
|
||||
|
||||
&:last-of-type {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:last-child {
|
||||
overflow: hidden;
|
||||
td {
|
||||
&:first-child {
|
||||
border-bottom-left-radius: 0.375rem;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom-right-radius: 0.375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mermaid {
|
||||
overflow-x: auto;
|
||||
}
|
81
projects/app/src/components/Markdown/index.tsx
Normal file
81
projects/app/src/components/Markdown/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
import RemarkMath from 'remark-math';
|
||||
import RehypeKatex from 'rehype-katex';
|
||||
import RemarkBreaks from 'remark-breaks';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import styles from './index.module.scss';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import CodeLight from './CodeLight';
|
||||
|
||||
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'));
|
||||
const MdImage = dynamic(() => import('./img/Image'));
|
||||
const ChatGuide = dynamic(() => import('./chat/Guide'));
|
||||
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'));
|
||||
const QuoteBlock = dynamic(() => import('./chat/Quote'));
|
||||
|
||||
export enum CodeClassName {
|
||||
guide = 'guide',
|
||||
mermaid = 'mermaid',
|
||||
echarts = 'echarts',
|
||||
quote = 'quote'
|
||||
}
|
||||
|
||||
function Code({ inline, className, children }: any) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const codeType = match?.[1];
|
||||
|
||||
if (codeType === CodeClassName.mermaid) {
|
||||
return <MermaidCodeBlock code={String(children)} />;
|
||||
}
|
||||
|
||||
if (codeType === CodeClassName.guide) {
|
||||
return <ChatGuide text={String(children)} />;
|
||||
}
|
||||
if (codeType === CodeClassName.echarts) {
|
||||
return <EChartsCodeBlock code={String(children)} />;
|
||||
}
|
||||
if (codeType === CodeClassName.quote) {
|
||||
return <QuoteBlock code={String(children)} />;
|
||||
}
|
||||
return (
|
||||
<CodeLight className={className} inline={inline} match={match}>
|
||||
{children}
|
||||
</CodeLight>
|
||||
);
|
||||
}
|
||||
function Image({ src }: { src?: string }) {
|
||||
return <MdImage src={src} />;
|
||||
}
|
||||
|
||||
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
img: Image,
|
||||
pre: 'div',
|
||||
p: 'div',
|
||||
code: Code
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}
|
||||
${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''}
|
||||
`}
|
||||
remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
|
||||
rehypePlugins={[RehypeKatex]}
|
||||
// @ts-ignore
|
||||
components={components}
|
||||
linkTarget={'_blank'}
|
||||
>
|
||||
{source}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default Markdown;
|
Reference in New Issue
Block a user