基于 shiy-temp分支修改
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
import React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import '../../styles/components/chat-with-llm/markdown.css';
|
||||
|
||||
const { Paragraph, Text, Title } = Typography;
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown 渲染组件
|
||||
* 简化版本,支持基本的 Markdown 语法
|
||||
*/
|
||||
export default function Markdown({ content, className = '' }: MarkdownProps) {
|
||||
if (!content) return null;
|
||||
|
||||
/**
|
||||
* 简单的 Markdown 解析器
|
||||
*/
|
||||
const parseMarkdown = (text: string): React.ReactNode => {
|
||||
// 分割成行
|
||||
const lines = text.split('\n');
|
||||
const elements: React.ReactNode[] = [];
|
||||
let currentParagraph: string[] = [];
|
||||
let inCodeBlock = false;
|
||||
let codeBlockContent: string[] = [];
|
||||
let codeBlockLanguage = '';
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (currentParagraph.length > 0) {
|
||||
const paragraphText = currentParagraph.join('\n');
|
||||
elements.push(
|
||||
<Paragraph key={elements.length} className={className}>
|
||||
{parseInlineElements(paragraphText)}
|
||||
</Paragraph>
|
||||
);
|
||||
currentParagraph = [];
|
||||
}
|
||||
};
|
||||
|
||||
const flushCodeBlock = () => {
|
||||
if (codeBlockContent.length > 0) {
|
||||
elements.push(
|
||||
<pre
|
||||
key={elements.length}
|
||||
className="bg-gray-800 text-white p-3 rounded-md overflow-x-auto my-2"
|
||||
>
|
||||
<code className={codeBlockLanguage ? `language-${codeBlockLanguage}` : ''}>
|
||||
{codeBlockContent.join('\n')}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
codeBlockContent = [];
|
||||
codeBlockLanguage = '';
|
||||
}
|
||||
};
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
// 处理代码块
|
||||
if (line.startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
// 结束代码块
|
||||
flushCodeBlock();
|
||||
inCodeBlock = false;
|
||||
} else {
|
||||
// 开始代码块
|
||||
flushParagraph();
|
||||
inCodeBlock = true;
|
||||
codeBlockLanguage = line.slice(3).trim();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeBlockContent.push(line);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理标题
|
||||
if (line.startsWith('# ')) {
|
||||
flushParagraph();
|
||||
elements.push(
|
||||
<Title key={elements.length} level={1} className={className}>
|
||||
{parseInlineElements(line.slice(2))}
|
||||
</Title>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.startsWith('## ')) {
|
||||
flushParagraph();
|
||||
elements.push(
|
||||
<Title key={elements.length} level={2} className={className}>
|
||||
{parseInlineElements(line.slice(3))}
|
||||
</Title>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.startsWith('### ')) {
|
||||
flushParagraph();
|
||||
elements.push(
|
||||
<Title key={elements.length} level={3} className={className}>
|
||||
{parseInlineElements(line.slice(4))}
|
||||
</Title>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理列表
|
||||
if (line.startsWith('- ') || line.startsWith('* ')) {
|
||||
flushParagraph();
|
||||
elements.push(
|
||||
<ul key={elements.length} className="list-disc list-inside my-2">
|
||||
<li>{parseInlineElements(line.slice(2))}</li>
|
||||
</ul>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^\d+\.\s/.test(line)) {
|
||||
flushParagraph();
|
||||
const match = line.match(/^\d+\.\s(.*)$/);
|
||||
if (match) {
|
||||
elements.push(
|
||||
<ol key={elements.length} className="list-decimal list-inside my-2">
|
||||
<li>{parseInlineElements(match[1])}</li>
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理空行
|
||||
if (line.trim() === '') {
|
||||
flushParagraph();
|
||||
return;
|
||||
}
|
||||
|
||||
// 普通段落
|
||||
currentParagraph.push(line);
|
||||
});
|
||||
|
||||
// 处理剩余内容
|
||||
flushParagraph();
|
||||
flushCodeBlock();
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析行内元素(粗体、斜体、代码、链接等)
|
||||
*/
|
||||
const parseInlineElements = (text: string): React.ReactNode => {
|
||||
const parts: React.ReactNode[] = [];
|
||||
let currentText = text;
|
||||
let key = 0;
|
||||
|
||||
// 处理行内代码
|
||||
currentText = currentText.replace(/`([^`]+)`/g, (match, code) => {
|
||||
const placeholder = `__CODE_${key}__`;
|
||||
parts.push(
|
||||
<Text key={`code-${key}`} code className="bg-gray-100 px-1 rounded">
|
||||
{code}
|
||||
</Text>
|
||||
);
|
||||
key++;
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 处理粗体
|
||||
currentText = currentText.replace(/\*\*([^*]+)\*\*/g, (match, bold) => {
|
||||
const placeholder = `__BOLD_${key}__`;
|
||||
parts.push(
|
||||
<Text key={`bold-${key}`} strong>
|
||||
{bold}
|
||||
</Text>
|
||||
);
|
||||
key++;
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 处理斜体
|
||||
currentText = currentText.replace(/\*([^*]+)\*/g, (match, italic) => {
|
||||
const placeholder = `__ITALIC_${key}__`;
|
||||
parts.push(
|
||||
<Text key={`italic-${key}`} italic>
|
||||
{italic}
|
||||
</Text>
|
||||
);
|
||||
key++;
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 处理链接
|
||||
currentText = currentText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
|
||||
const placeholder = `__LINK_${key}__`;
|
||||
parts.push(
|
||||
<a
|
||||
key={`link-${key}`}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-700 underline"
|
||||
>
|
||||
{linkText}
|
||||
</a>
|
||||
);
|
||||
key++;
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 重新组装文本
|
||||
const finalParts: React.ReactNode[] = [];
|
||||
const textParts = currentText.split(/(__(?:CODE|BOLD|ITALIC|LINK)_\d+__)/);
|
||||
|
||||
textParts.forEach((part, index) => {
|
||||
const match = part.match(/^__(\w+)_(\d+)__$/);
|
||||
if (match) {
|
||||
const [, type, partKey] = match;
|
||||
const component = parts.find((p: any) =>
|
||||
p.key === `${type.toLowerCase()}-${partKey}`
|
||||
);
|
||||
if (component) {
|
||||
finalParts.push(component);
|
||||
}
|
||||
} else if (part) {
|
||||
finalParts.push(part);
|
||||
}
|
||||
});
|
||||
|
||||
return finalParts.length === 1 ? finalParts[0] : finalParts;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`markdown-content ${className}`}>
|
||||
{parseMarkdown(content)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user