diff --git a/web/package.json b/web/package.json
index 2abb1897..a09a58af 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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",
diff --git a/web/src/components/common/MarkdownRenderer.js b/web/src/components/common/MarkdownRenderer.js
new file mode 100644
index 00000000..12662265
--- /dev/null
+++ b/web/src/components/common/MarkdownRenderer.js
@@ -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 (
+
viewSvgInNewWindow()}
+ >
+ {props.code}
+
+ );
+}
+
+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(' {
+ 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 (
+ <>
+
+
+
+ }
+ 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)',
+ }}
+ />
+
+
+ {props.children}
+
+ {mermaidCode.length > 0 && (
+
+ )}
+ {htmlCode.length > 0 && (
+
+ )}
+ >
+ );
+}
+
+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 (
+
+
+
+ );
+ }
+ return null;
+ };
+
+ return (
+
+
+ {props.children}
+
+ {renderShowMoreButton()}
+
+ );
+}
+
+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]*?)()/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 (
+ ,
+ a: (aProps) => {
+ const href = aProps.href || '';
+ if (/\.(aac|mp3|opus|wav)$/.test(href)) {
+ return (
+
+
+
+ );
+ }
+ if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
+ return (
+
+ );
+ }
+ const isInternal = /^\/#/i.test(href);
+ const target = isInternal ? '_self' : aProps.target ?? '_blank';
+ return (
+ {
+ e.target.style.textDecoration = 'underline';
+ }}
+ onMouseLeave={(e) => {
+ e.target.style.textDecoration = 'none';
+ }}
+ />
+ );
+ },
+ h1: (props) => ,
+ h2: (props) => ,
+ h3: (props) => ,
+ h4: (props) => ,
+ h5: (props) => ,
+ h6: (props) => ,
+ blockquote: (props) => (
+
+ ),
+ ul: (props) => ,
+ ol: (props) =>
,
+ li: (props) => ,
+ table: (props) => (
+
+ ),
+ th: (props) => (
+ |
+ ),
+ td: (props) => (
+ |
+ ),
+ }}
+ >
+ {escapedContent}
+
+ );
+}
+
+export const MarkdownContent = React.memo(_MarkdownContent);
+
+export function MarkdownRenderer(props) {
+ const {
+ content,
+ loading,
+ fontSize = 14,
+ fontFamily = 'inherit',
+ className,
+ style,
+ ...otherProps
+ } = props;
+
+ return (
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default MarkdownRenderer;
\ No newline at end of file
diff --git a/web/src/components/common/markdown.css b/web/src/components/common/markdown.css
new file mode 100644
index 00000000..e40295d7
--- /dev/null
+++ b/web/src/components/common/markdown.css
@@ -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;
+}
\ No newline at end of file
diff --git a/web/src/components/playground/MessageContent.js b/web/src/components/playground/MessageContent.js
index 52aa1577..6d0e3974 100644
--- a/web/src/components/playground/MessageContent.js
+++ b/web/src/components/playground/MessageContent.js
@@ -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 = ({
@@ -304,8 +306,11 @@ const MessageContent = ({
{/* 显示文本内容 */}
{textContent && textContent.text && typeof textContent.text === 'string' && textContent.text.trim() !== '' && (
-
@@ -317,14 +322,20 @@ const MessageContent = ({
if (finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') {
return (
-
+
);
}
} else {
return (
-