更新AI聊天页面样式

This commit is contained in:
pingchuan
2025-06-06 15:07:57 +08:00
parent d4ad36c3f2
commit 1b79f973da
17 changed files with 11257 additions and 2813 deletions
+88 -241
View File
@@ -1,242 +1,89 @@
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>
);
import React from 'react';
import ReactMarkdown from 'react-markdown';
import 'katex/dist/katex.min.css';
import RemarkMath from 'remark-math';
import RemarkBreaks from 'remark-breaks';
import RehypeKatex from 'rehype-katex';
import RemarkGfm from 'remark-gfm';
import '../../styles/components/chat-with-llm/markdown.css';
interface MarkdownProps {
content: string;
className?: string;
}
/**
* Markdown 渲染组件
* 使用 react-markdown 库进行标准 Markdown 解析,支持流式渲染
*/
export default function Markdown({ content, className = '' }: MarkdownProps) {
console.log('🎨 [Markdown] 渲染组件:', {
contentLength: content?.length || 0,
contentPreview: content?.substring(0, 100) + (content && content.length > 100 ? '...' : ''),
className,
hasContent: !!content
});
if (!content) {
console.log('⚠️ [Markdown] 内容为空,返回null');
return null;
}
return (
<div className={`markdown-content ${className}`}>
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[RehypeKatex]}
components={{
code({ className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || '');
const isCodeBlock = match;
if (isCodeBlock) {
// 代码块
return (
<pre style={{
backgroundColor: '#f6f8fa',
padding: '16px',
borderRadius: '6px',
overflow: 'auto',
fontSize: '14px',
lineHeight: '1.45',
margin: '1em 0'
}}>
<code style={{
backgroundColor: 'transparent',
padding: '0',
fontSize: 'inherit',
fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace'
}}>
{String(children).replace(/\n$/, '')}
</code>
</pre>
);
} else {
// 内联代码
return (
<code
className={className}
style={{
backgroundColor: 'rgba(175, 184, 193, 0.2)',
padding: '0.2em 0.4em',
borderRadius: '3px',
fontSize: '85%',
fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace'
}}
{...props}
>
{children}
</code>
);
}
},
}}
>
{content}
</ReactMarkdown>
</div>
);
}