✨ feat(markdown): replace Semi UI MarkdownRender with react-markdown for enhanced rendering
- Replace Semi UI's MarkdownRender with react-markdown library for better performance and features - Add comprehensive markdown rendering support including: * Math formulas with KaTeX * Code syntax highlighting with rehype-highlight * Mermaid diagrams support * GitHub Flavored Markdown (tables, task lists, etc.) * HTML preview for code blocks * Media file support (audio/video) - Create new MarkdownRenderer component with enhanced features: * Copy code button with hover effects * Code folding for long code blocks * Responsive design for mobile devices - Add white text styling for user messages to improve readability on blue backgrounds - Update all components using markdown rendering: * MessageContent.js - playground chat messages * About/index.js - about page content * Home/index.js - home page content * NoticeModal.js - system notice modal * OtherSetting.js - settings page - Install new dependencies: react-markdown, remark-math, remark-breaks, remark-gfm, rehype-katex, rehype-highlight, katex, mermaid, use-debounce, clsx - Add comprehensive CSS styles in markdown.css for better theming and user experience - Remove unused imports and optimize component imports Breaking changes: None - maintains backward compatibility with existing markdown content
This commit is contained in:
@@ -11,26 +11,36 @@
|
||||
"@visactor/vchart": "~1.8.8",
|
||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||
"axios": "^0.27.2",
|
||||
"clsx": "^2.1.1",
|
||||
"country-flag-icons": "^1.5.19",
|
||||
"dayjs": "^1.11.11",
|
||||
"history": "^5.3.0",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"katex": "^0.16.22",
|
||||
"lucide-react": "^0.511.0",
|
||||
"marked": "^4.1.1",
|
||||
"mermaid": "^11.6.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-fireworks": "^1.0.4",
|
||||
"react-i18next": "^13.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-telegram-login": "^1.1.2",
|
||||
"react-toastify": "^9.0.8",
|
||||
"react-turnstile": "^1.0.5",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"semantic-ui-offline": "^2.5.0",
|
||||
"semantic-ui-react": "^2.1.3",
|
||||
"sse": "https://github.com/mpetazzoni/sse.js"
|
||||
"sse": "https://github.com/mpetazzoni/sse.js",
|
||||
"use-debounce": "^10.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
491
web/src/components/common/MarkdownRenderer.js
Normal file
491
web/src/components/common/MarkdownRenderer.js
Normal file
@@ -0,0 +1,491 @@
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import './markdown.css';
|
||||
import RemarkMath from 'remark-math';
|
||||
import RemarkBreaks from 'remark-breaks';
|
||||
import RehypeKatex from 'rehype-katex';
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
import RehypeHighlight from 'rehype-highlight';
|
||||
import { useRef, useState, useEffect, useMemo } from 'react';
|
||||
import mermaid from 'mermaid';
|
||||
import React from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import clsx from 'clsx';
|
||||
import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
|
||||
import { copy } from '../../helpers/utils';
|
||||
import { IconCopy } from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
securityLevel: 'loose',
|
||||
});
|
||||
|
||||
export function Mermaid(props) {
|
||||
const ref = useRef(null);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.code && ref.current) {
|
||||
mermaid
|
||||
.run({
|
||||
nodes: [ref.current],
|
||||
suppressErrors: true,
|
||||
})
|
||||
.catch((e) => {
|
||||
setHasError(true);
|
||||
console.error('[Mermaid] ', e.message);
|
||||
});
|
||||
}
|
||||
}, [props.code]);
|
||||
|
||||
function viewSvgInNewWindow() {
|
||||
const svg = ref.current?.querySelector('svg');
|
||||
if (!svg) return;
|
||||
const text = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([text], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('mermaid-container')}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
overflow: 'auto',
|
||||
padding: '12px',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--semi-color-bg-1)',
|
||||
margin: '12px 0',
|
||||
}}
|
||||
ref={ref}
|
||||
onClick={() => viewSvgInNewWindow()}
|
||||
>
|
||||
{props.code}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreCode(props) {
|
||||
const ref = useRef(null);
|
||||
const [mermaidCode, setMermaidCode] = useState('');
|
||||
const [htmlCode, setHtmlCode] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderArtifacts = useDebouncedCallback(() => {
|
||||
if (!ref.current) return;
|
||||
const mermaidDom = ref.current.querySelector('code.language-mermaid');
|
||||
if (mermaidDom) {
|
||||
setMermaidCode(mermaidDom.innerText);
|
||||
}
|
||||
const htmlDom = ref.current.querySelector('code.language-html');
|
||||
const refText = ref.current.querySelector('code')?.innerText;
|
||||
if (htmlDom) {
|
||||
setHtmlCode(htmlDom.innerText);
|
||||
} else if (
|
||||
refText?.startsWith('<!DOCTYPE') ||
|
||||
refText?.startsWith('<svg') ||
|
||||
refText?.startsWith('<?xml')
|
||||
) {
|
||||
setHtmlCode(refText);
|
||||
}
|
||||
}, 600);
|
||||
|
||||
// 处理代码块的换行
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const codeElements = ref.current.querySelectorAll('code');
|
||||
const wrapLanguages = [
|
||||
'',
|
||||
'md',
|
||||
'markdown',
|
||||
'text',
|
||||
'txt',
|
||||
'plaintext',
|
||||
'tex',
|
||||
'latex',
|
||||
];
|
||||
codeElements.forEach((codeElement) => {
|
||||
let languageClass = codeElement.className.match(/language-(\w+)/);
|
||||
let name = languageClass ? languageClass[1] : '';
|
||||
if (wrapLanguages.includes(name)) {
|
||||
codeElement.style.whiteSpace = 'pre-wrap';
|
||||
}
|
||||
});
|
||||
setTimeout(renderArtifacts, 1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<pre
|
||||
ref={ref}
|
||||
style={{
|
||||
position: 'relative',
|
||||
backgroundColor: 'var(--semi-color-fill-0)',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: '6px',
|
||||
padding: '12px',
|
||||
margin: '12px 0',
|
||||
overflow: 'auto',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="copy-code-button"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '8px',
|
||||
right: '8px',
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
zIndex: 10,
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Tooltip content={t('复制代码')}>
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
icon={<IconCopy />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (ref.current) {
|
||||
const code = ref.current.querySelector('code')?.innerText ?? '';
|
||||
copy(code).then((success) => {
|
||||
if (success) {
|
||||
Toast.success(t('代码已复制到剪贴板'));
|
||||
} else {
|
||||
Toast.error(t('复制失败,请手动复制'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: '4px',
|
||||
backgroundColor: 'var(--semi-color-bg-2)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{props.children}
|
||||
</pre>
|
||||
{mermaidCode.length > 0 && (
|
||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||
)}
|
||||
{htmlCode.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
margin: '12px 0',
|
||||
backgroundColor: 'var(--semi-color-bg-1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '8px', fontSize: '12px', color: 'var(--semi-color-text-2)' }}>
|
||||
HTML预览:
|
||||
</div>
|
||||
<div dangerouslySetInnerHTML={{ __html: htmlCode }} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomCode(props) {
|
||||
const ref = useRef(null);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [showToggle, setShowToggle] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const codeHeight = ref.current.scrollHeight;
|
||||
setShowToggle(codeHeight > 400);
|
||||
ref.current.scrollTop = ref.current.scrollHeight;
|
||||
}
|
||||
}, [props.children]);
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed((collapsed) => !collapsed);
|
||||
};
|
||||
|
||||
const renderShowMoreButton = () => {
|
||||
if (showToggle && collapsed) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '8px',
|
||||
right: '8px',
|
||||
left: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Button size="small" onClick={toggleCollapsed} theme="solid">
|
||||
{t('显示更多')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<code
|
||||
className={clsx(props?.className)}
|
||||
ref={ref}
|
||||
style={{
|
||||
maxHeight: collapsed ? '400px' : 'none',
|
||||
overflowY: 'hidden',
|
||||
display: 'block',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: 'var(--semi-color-fill-0)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</code>
|
||||
{renderShowMoreButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function escapeBrackets(text) {
|
||||
const pattern =
|
||||
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
|
||||
return text.replace(
|
||||
pattern,
|
||||
(match, codeBlock, squareBracket, roundBracket) => {
|
||||
if (codeBlock) {
|
||||
return codeBlock;
|
||||
} else if (squareBracket) {
|
||||
return `$$${squareBracket}$$`;
|
||||
} else if (roundBracket) {
|
||||
return `$${roundBracket}$`;
|
||||
}
|
||||
return match;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function tryWrapHtmlCode(text) {
|
||||
// 尝试包装HTML代码
|
||||
if (text.includes('```')) {
|
||||
return text;
|
||||
}
|
||||
return text
|
||||
.replace(
|
||||
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
|
||||
(match, quoteStart, lang, newLine, doctype) => {
|
||||
return !quoteStart ? '\n```html\n' + doctype : match;
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
|
||||
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
|
||||
return !quoteEnd ? bodyEnd + space + htmlEnd + '\n```\n' : match;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function _MarkdownContent(props) {
|
||||
const escapedContent = useMemo(() => {
|
||||
return tryWrapHtmlCode(escapeBrackets(props.content));
|
||||
}, [props.content]);
|
||||
|
||||
// 判断是否为用户消息
|
||||
const isUserMessage = props.className && props.className.includes('user-message');
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
[
|
||||
RehypeHighlight,
|
||||
{
|
||||
detect: false,
|
||||
ignoreMissing: true,
|
||||
},
|
||||
],
|
||||
]}
|
||||
components={{
|
||||
pre: PreCode,
|
||||
code: CustomCode,
|
||||
p: (pProps) => <p {...pProps} dir="auto" style={{ lineHeight: '1.6', color: isUserMessage ? 'white' : 'inherit' }} />,
|
||||
a: (aProps) => {
|
||||
const href = aProps.href || '';
|
||||
if (/\.(aac|mp3|opus|wav)$/.test(href)) {
|
||||
return (
|
||||
<figure style={{ margin: '12px 0' }}>
|
||||
<audio controls src={href} style={{ width: '100%' }}></audio>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
|
||||
return (
|
||||
<video controls style={{ width: '100%', maxWidth: '100%', margin: '12px 0' }}>
|
||||
<source src={href} />
|
||||
</video>
|
||||
);
|
||||
}
|
||||
const isInternal = /^\/#/i.test(href);
|
||||
const target = isInternal ? '_self' : aProps.target ?? '_blank';
|
||||
return (
|
||||
<a
|
||||
{...aProps}
|
||||
target={target}
|
||||
style={{
|
||||
color: isUserMessage ? '#87CEEB' : 'var(--semi-color-primary)',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.textDecoration = 'underline';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.textDecoration = 'none';
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
h1: (props) => <h1 {...props} style={{ fontSize: '24px', fontWeight: 'bold', margin: '20px 0 12px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
||||
h2: (props) => <h2 {...props} style={{ fontSize: '20px', fontWeight: 'bold', margin: '18px 0 10px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
||||
h3: (props) => <h3 {...props} style={{ fontSize: '18px', fontWeight: 'bold', margin: '16px 0 8px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
||||
h4: (props) => <h4 {...props} style={{ fontSize: '16px', fontWeight: 'bold', margin: '14px 0 6px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
||||
h5: (props) => <h5 {...props} style={{ fontSize: '14px', fontWeight: 'bold', margin: '12px 0 4px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
||||
h6: (props) => <h6 {...props} style={{ fontSize: '13px', fontWeight: 'bold', margin: '10px 0 4px 0', color: isUserMessage ? 'white' : 'var(--semi-color-text-0)' }} />,
|
||||
blockquote: (props) => (
|
||||
<blockquote
|
||||
{...props}
|
||||
style={{
|
||||
borderLeft: isUserMessage ? '4px solid rgba(255, 255, 255, 0.5)' : '4px solid var(--semi-color-primary)',
|
||||
paddingLeft: '16px',
|
||||
margin: '12px 0',
|
||||
backgroundColor: isUserMessage ? 'rgba(255, 255, 255, 0.1)' : 'var(--semi-color-fill-0)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '0 4px 4px 0',
|
||||
fontStyle: 'italic',
|
||||
color: isUserMessage ? 'white' : 'inherit',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
ul: (props) => <ul {...props} style={{ margin: '8px 0', paddingLeft: '20px', color: isUserMessage ? 'white' : 'inherit' }} />,
|
||||
ol: (props) => <ol {...props} style={{ margin: '8px 0', paddingLeft: '20px', color: isUserMessage ? 'white' : 'inherit' }} />,
|
||||
li: (props) => <li {...props} style={{ margin: '4px 0', lineHeight: '1.6', color: isUserMessage ? 'white' : 'inherit' }} />,
|
||||
table: (props) => (
|
||||
<div style={{ overflow: 'auto', margin: '12px 0' }}>
|
||||
<table
|
||||
{...props}
|
||||
style={{
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
th: (props) => (
|
||||
<th
|
||||
{...props}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: isUserMessage ? 'rgba(255, 255, 255, 0.2)' : 'var(--semi-color-fill-1)',
|
||||
border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'left',
|
||||
color: isUserMessage ? 'white' : 'inherit',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
td: (props) => (
|
||||
<td
|
||||
{...props}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: isUserMessage ? '1px solid rgba(255, 255, 255, 0.3)' : '1px solid var(--semi-color-border)',
|
||||
color: isUserMessage ? 'white' : 'inherit',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{escapedContent}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
|
||||
export const MarkdownContent = React.memo(_MarkdownContent);
|
||||
|
||||
export function MarkdownRenderer(props) {
|
||||
const {
|
||||
content,
|
||||
loading,
|
||||
fontSize = 14,
|
||||
fontFamily = 'inherit',
|
||||
className,
|
||||
style,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('markdown-body', className)}
|
||||
style={{
|
||||
fontSize: `${fontSize}px`,
|
||||
fontFamily: fontFamily,
|
||||
lineHeight: '1.6',
|
||||
color: 'var(--semi-color-text-0)',
|
||||
...style,
|
||||
}}
|
||||
dir="auto"
|
||||
{...otherProps}
|
||||
>
|
||||
{loading ? (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '16px',
|
||||
color: 'var(--semi-color-text-2)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
border: '2px solid var(--semi-color-border)',
|
||||
borderTop: '2px solid var(--semi-color-primary)',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
}} />
|
||||
正在渲染...
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownContent content={content} className={className} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MarkdownRenderer;
|
||||
422
web/src/components/common/markdown.css
Normal file
422
web/src/components/common/markdown.css
Normal file
@@ -0,0 +1,422 @@
|
||||
/* 基础markdown样式 */
|
||||
.markdown-body {
|
||||
font-family: inherit;
|
||||
line-height: 1.6;
|
||||
color: var(--semi-color-text-0);
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 用户消息样式 - 白色字体适配蓝色背景 */
|
||||
.user-message {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.user-message .markdown-body {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.user-message h1,
|
||||
.user-message h2,
|
||||
.user-message h3,
|
||||
.user-message h4,
|
||||
.user-message h5,
|
||||
.user-message h6 {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.user-message p {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.user-message span {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.user-message div {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.user-message li {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.user-message td,
|
||||
.user-message th {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.user-message blockquote {
|
||||
color: white !important;
|
||||
border-left-color: rgba(255, 255, 255, 0.5) !important;
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.user-message code:not(pre code) {
|
||||
color: #000 !important;
|
||||
background-color: rgba(255, 255, 255, 0.9) !important;
|
||||
}
|
||||
|
||||
.user-message a {
|
||||
color: #87CEEB !important;
|
||||
/* 浅蓝色链接 */
|
||||
}
|
||||
|
||||
.user-message a:hover {
|
||||
color: #B0E0E6 !important;
|
||||
/* hover时更浅的蓝色 */
|
||||
}
|
||||
|
||||
/* 表格在用户消息中的样式 */
|
||||
.user-message table {
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.user-message th {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.user-message td {
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 代码高亮主题 - 适配Semi Design */
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--semi-color-text-0);
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: var(--semi-color-text-2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-subst {
|
||||
color: var(--semi-color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-literal,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag .hljs-attr {
|
||||
color: var(--semi-color-warning);
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-doctag {
|
||||
color: var(--semi-color-success);
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-selector-id {
|
||||
color: var(--semi-color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-subst {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-type,
|
||||
.hljs-class .hljs-title {
|
||||
color: var(--semi-color-info);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-attribute {
|
||||
color: var(--semi-color-primary);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
.hljs-link {
|
||||
color: var(--semi-color-tertiary);
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: var(--semi-color-warning);
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name {
|
||||
color: var(--semi-color-info);
|
||||
}
|
||||
|
||||
.hljs-meta {
|
||||
color: var(--semi-color-text-2);
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: var(--semi-color-danger-light-default);
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
background: var(--semi-color-success-light-default);
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Mermaid容器样式 */
|
||||
.mermaid-container {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mermaid-container:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 代码块样式增强 */
|
||||
pre {
|
||||
position: relative;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
pre:hover {
|
||||
border-color: var(--semi-color-primary) !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
pre:hover .copy-code-button {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.copy-code-button {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 10;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.copy-code-button:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.copy-code-button button {
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
/* 确保按钮可点击 */
|
||||
.copy-code-button .semi-button {
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.copy-code-button .semi-button:hover {
|
||||
background-color: var(--semi-color-fill-1) !important;
|
||||
border-color: var(--semi-color-primary) !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 表格响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.markdown-body table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown-body th,
|
||||
.markdown-body td {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 数学公式样式 */
|
||||
.katex {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.katex-display {
|
||||
margin: 1em 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 链接hover效果 */
|
||||
.markdown-body a {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* 引用块样式增强 */
|
||||
.markdown-body blockquote {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-body blockquote::before {
|
||||
content: '"';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: -8px;
|
||||
font-size: 24px;
|
||||
color: var(--semi-color-primary);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* 列表样式增强 */
|
||||
.markdown-body ul li::marker {
|
||||
color: var(--semi-color-primary);
|
||||
}
|
||||
|
||||
.markdown-body ol li::marker {
|
||||
color: var(--semi-color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 分隔线样式 */
|
||||
.markdown-body hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, var(--semi-color-border), transparent);
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
/* 图片样式 */
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
/* 内联代码样式 */
|
||||
.markdown-body code:not(pre code) {
|
||||
background-color: var(--semi-color-fill-1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
color: var(--semi-color-primary);
|
||||
border: 1px solid var(--semi-color-border);
|
||||
}
|
||||
|
||||
/* 标题锚点样式 */
|
||||
.markdown-body h1:hover,
|
||||
.markdown-body h2:hover,
|
||||
.markdown-body h3:hover,
|
||||
.markdown-body h4:hover,
|
||||
.markdown-body h5:hover,
|
||||
.markdown-body h6:hover {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 任务列表样式 */
|
||||
.markdown-body input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.markdown-body li.task-list-item {
|
||||
list-style: none;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
/* 键盘按键样式 */
|
||||
.markdown-body kbd {
|
||||
background-color: var(--semi-color-fill-0);
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 var(--semi-color-border);
|
||||
color: var(--semi-color-text-0);
|
||||
display: inline-block;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 详情折叠样式 */
|
||||
.markdown-body details {
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown-body summary {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
color: var(--semi-color-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-body summary:hover {
|
||||
color: var(--semi-color-primary-hover);
|
||||
}
|
||||
|
||||
/* 脚注样式 */
|
||||
.markdown-body .footnote-ref {
|
||||
color: var(--semi-color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.markdown-body .footnote-ref:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 警告块样式 */
|
||||
.markdown-body .warning {
|
||||
background-color: var(--semi-color-warning-light-default);
|
||||
border-left: 4px solid var(--semi-color-warning);
|
||||
padding: 12px 16px;
|
||||
margin: 12px 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.markdown-body .info {
|
||||
background-color: var(--semi-color-info-light-default);
|
||||
border-left: 4px solid var(--semi-color-info);
|
||||
padding: 12px 16px;
|
||||
margin: 12px 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.markdown-body .success {
|
||||
background-color: var(--semi-color-success-light-default);
|
||||
border-left: 4px solid var(--semi-color-success);
|
||||
padding: 12px 16px;
|
||||
margin: 12px 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.markdown-body .danger {
|
||||
background-color: var(--semi-color-danger-light-default);
|
||||
border-left: 4px solid var(--semi-color-danger);
|
||||
padding: 12px 16px;
|
||||
margin: 12px 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Typography,
|
||||
MarkdownRender,
|
||||
TextArea,
|
||||
Button,
|
||||
Space,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import MarkdownRenderer from '../common/MarkdownRenderer';
|
||||
import {
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
@@ -218,7 +217,10 @@ const MessageContent = ({
|
||||
<div className="p-3 sm:p-5 pt-2 sm:pt-4">
|
||||
<div className="bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto max-h-50 overflow-y-auto">
|
||||
<div className="prose prose-xs sm:prose-sm prose-purple max-w-none text-xs sm:text-sm">
|
||||
<MarkdownRender raw={finalExtractedThinkingContent} />
|
||||
<MarkdownRenderer
|
||||
content={finalExtractedThinkingContent}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,8 +306,11 @@ const MessageContent = ({
|
||||
|
||||
{/* 显示文本内容 */}
|
||||
{textContent && textContent.text && typeof textContent.text === 'string' && textContent.text.trim() !== '' && (
|
||||
<div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
|
||||
<MarkdownRender raw={textContent.text} />
|
||||
<div className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}>
|
||||
<MarkdownRenderer
|
||||
content={textContent.text}
|
||||
className={message.role === 'user' ? 'user-message' : ''}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -317,14 +322,20 @@ const MessageContent = ({
|
||||
if (finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') {
|
||||
return (
|
||||
<div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
|
||||
<MarkdownRender raw={finalDisplayableFinalContent} />
|
||||
<MarkdownRenderer
|
||||
content={finalDisplayableFinalContent}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
|
||||
<MarkdownRender raw={message.content} />
|
||||
<div className={`prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm ${message.role === 'user' ? 'user-message' : ''}`}>
|
||||
<MarkdownRenderer
|
||||
content={message.content}
|
||||
className={message.role === 'user' ? 'user-message' : ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user