167 lines
5.2 KiB
JavaScript
167 lines
5.2 KiB
JavaScript
// src/components/ChatBot/MarkdownWithCharts.js
|
|
// 支持 ECharts 图表的 Markdown 渲染组件
|
|
|
|
import React from 'react';
|
|
import { Box, Alert, AlertIcon, Text, VStack, Code } from '@chakra-ui/react';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import { EChartsRenderer } from './EChartsRenderer';
|
|
import { logger } from '@utils/logger';
|
|
|
|
/**
|
|
* 解析 Markdown 内容,提取 ECharts 代码块
|
|
* @param {string} markdown - Markdown 文本
|
|
* @returns {Array} - 包含文本和图表的数组
|
|
*/
|
|
const parseMarkdownWithCharts = (markdown) => {
|
|
if (!markdown) return [];
|
|
|
|
const parts = [];
|
|
const echartsRegex = /```echarts\s*\n([\s\S]*?)```/g;
|
|
let lastIndex = 0;
|
|
let match;
|
|
|
|
while ((match = echartsRegex.exec(markdown)) !== null) {
|
|
// 添加代码块前的文本
|
|
if (match.index > lastIndex) {
|
|
const textBefore = markdown.substring(lastIndex, match.index).trim();
|
|
if (textBefore) {
|
|
parts.push({ type: 'text', content: textBefore });
|
|
}
|
|
}
|
|
|
|
// 添加 ECharts 配置
|
|
const chartConfig = match[1].trim();
|
|
parts.push({ type: 'chart', content: chartConfig });
|
|
|
|
lastIndex = match.index + match[0].length;
|
|
}
|
|
|
|
// 添加剩余文本
|
|
if (lastIndex < markdown.length) {
|
|
const textAfter = markdown.substring(lastIndex).trim();
|
|
if (textAfter) {
|
|
parts.push({ type: 'text', content: textAfter });
|
|
}
|
|
}
|
|
|
|
// 如果没有找到图表,返回整个 markdown 作为文本
|
|
if (parts.length === 0) {
|
|
parts.push({ type: 'text', content: markdown });
|
|
}
|
|
|
|
return parts;
|
|
};
|
|
|
|
/**
|
|
* 支持 ECharts 图表的 Markdown 渲染组件
|
|
* @param {string} content - Markdown 文本
|
|
*/
|
|
export const MarkdownWithCharts = ({ content }) => {
|
|
const parts = parseMarkdownWithCharts(content);
|
|
|
|
return (
|
|
<VStack align="stretch" spacing={4}>
|
|
{parts.map((part, index) => {
|
|
if (part.type === 'text') {
|
|
// 渲染普通 Markdown
|
|
return (
|
|
<Box key={index}>
|
|
<ReactMarkdown
|
|
components={{
|
|
// 自定义渲染样式
|
|
p: ({ children }) => (
|
|
<Text mb={2} fontSize="sm">
|
|
{children}
|
|
</Text>
|
|
),
|
|
h1: ({ children }) => (
|
|
<Text fontSize="xl" fontWeight="bold" mb={3}>
|
|
{children}
|
|
</Text>
|
|
),
|
|
h2: ({ children }) => (
|
|
<Text fontSize="lg" fontWeight="bold" mb={2}>
|
|
{children}
|
|
</Text>
|
|
),
|
|
h3: ({ children }) => (
|
|
<Text fontSize="md" fontWeight="bold" mb={2}>
|
|
{children}
|
|
</Text>
|
|
),
|
|
ul: ({ children }) => (
|
|
<Box as="ul" pl={4} mb={2}>
|
|
{children}
|
|
</Box>
|
|
),
|
|
ol: ({ children }) => (
|
|
<Box as="ol" pl={4} mb={2}>
|
|
{children}
|
|
</Box>
|
|
),
|
|
li: ({ children }) => (
|
|
<Box as="li" fontSize="sm" mb={1}>
|
|
{children}
|
|
</Box>
|
|
),
|
|
code: ({ inline, children }) =>
|
|
inline ? (
|
|
<Code fontSize="sm" px={1}>
|
|
{children}
|
|
</Code>
|
|
) : (
|
|
<Code display="block" p={3} borderRadius="md" fontSize="sm" whiteSpace="pre-wrap">
|
|
{children}
|
|
</Code>
|
|
),
|
|
blockquote: ({ children }) => (
|
|
<Box
|
|
borderLeftWidth="4px"
|
|
borderLeftColor="blue.500"
|
|
pl={4}
|
|
py={2}
|
|
fontStyle="italic"
|
|
color="gray.600"
|
|
>
|
|
{children}
|
|
</Box>
|
|
),
|
|
}}
|
|
>
|
|
{part.content}
|
|
</ReactMarkdown>
|
|
</Box>
|
|
);
|
|
} else if (part.type === 'chart') {
|
|
// 渲染 ECharts 图表
|
|
try {
|
|
const chartOption = JSON.parse(part.content);
|
|
return (
|
|
<Box key={index}>
|
|
<EChartsRenderer option={chartOption} height={350} />
|
|
</Box>
|
|
);
|
|
} catch (error) {
|
|
logger.error('解析 ECharts 配置失败', error, part.content);
|
|
return (
|
|
<Alert status="warning" key={index} borderRadius="md">
|
|
<AlertIcon />
|
|
<VStack align="flex-start" spacing={1}>
|
|
<Text fontSize="sm" fontWeight="bold">
|
|
图表配置解析失败
|
|
</Text>
|
|
<Code fontSize="xs" maxW="100%" overflow="auto">
|
|
{part.content.substring(0, 200)}
|
|
{part.content.length > 200 ? '...' : ''}
|
|
</Code>
|
|
</VStack>
|
|
</Alert>
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
})}
|
|
</VStack>
|
|
);
|
|
};
|