agent功能开发增加MCP后端
This commit is contained in:
72
src/components/ChatBot/EChartsRenderer.js
Normal file
72
src/components/ChatBot/EChartsRenderer.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// src/components/ChatBot/EChartsRenderer.js
|
||||
// ECharts 图表渲染组件
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Box, useColorModeValue } from '@chakra-ui/react';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
/**
|
||||
* ECharts 图表渲染组件
|
||||
* @param {Object} option - ECharts 配置对象
|
||||
* @param {number} height - 图表高度(默认 400px)
|
||||
*/
|
||||
export const EChartsRenderer = ({ option, height = 400 }) => {
|
||||
const chartRef = useRef(null);
|
||||
const chartInstance = useRef(null);
|
||||
const bgColor = useColorModeValue('white', 'gray.800');
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !option) return;
|
||||
|
||||
// 初始化图表
|
||||
if (!chartInstance.current) {
|
||||
chartInstance.current = echarts.init(chartRef.current);
|
||||
}
|
||||
|
||||
// 设置默认主题配置
|
||||
const defaultOption = {
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
...option,
|
||||
};
|
||||
|
||||
// 设置图表配置
|
||||
chartInstance.current.setOption(defaultOption, true);
|
||||
|
||||
// 响应式调整大小
|
||||
const handleResize = () => {
|
||||
chartInstance.current?.resize();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
// chartInstance.current?.dispose(); // 不要销毁,避免重新渲染时闪烁
|
||||
};
|
||||
}, [option]);
|
||||
|
||||
// 组件卸载时销毁图表
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
chartInstance.current?.dispose();
|
||||
chartInstance.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={chartRef}
|
||||
width="100%"
|
||||
height={`${height}px`}
|
||||
bg={bgColor}
|
||||
borderRadius="md"
|
||||
boxShadow="sm"
|
||||
/>
|
||||
);
|
||||
};
|
||||
166
src/components/ChatBot/MarkdownWithCharts.js
Normal file
166
src/components/ChatBot/MarkdownWithCharts.js
Normal file
@@ -0,0 +1,166 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
import { PlanCard } from '@components/ChatBot/PlanCard';
|
||||
import { StepResultCard } from '@components/ChatBot/StepResultCard';
|
||||
import { MarkdownWithCharts } from '@components/ChatBot/MarkdownWithCharts';
|
||||
import { logger } from '@utils/logger';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -312,25 +313,15 @@ const AgentChatV3 = () => {
|
||||
}
|
||||
|
||||
// 显示执行步骤
|
||||
if (data.steps && data.steps.length > 0) {
|
||||
stepResults = data.steps;
|
||||
addMessage({
|
||||
type: MessageTypes.AGENT_EXECUTING,
|
||||
content: '正在执行步骤...',
|
||||
plan: currentPlan,
|
||||
stepResults: stepResults,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
if (data.step_results && data.step_results.length > 0) {
|
||||
stepResults = data.step_results;
|
||||
setCurrentProgress(70);
|
||||
}
|
||||
|
||||
// 移除执行中消息
|
||||
setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING));
|
||||
|
||||
// 显示最终结果
|
||||
// 显示最终结果(包含执行步骤)
|
||||
addMessage({
|
||||
type: MessageTypes.AGENT_RESPONSE,
|
||||
content: data.final_answer || data.message || '处理完成',
|
||||
content: data.final_summary || data.message || '处理完成',
|
||||
plan: currentPlan,
|
||||
stepResults: stepResults,
|
||||
metadata: data.metadata,
|
||||
@@ -801,7 +792,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
||||
<HStack align="flex-start" maxW="85%">
|
||||
<Avatar size="sm" bg="green.500" icon={<FiCpu fontSize="1rem" />} />
|
||||
<VStack align="stretch" flex="1" spacing={3}>
|
||||
{/* 最终总结 */}
|
||||
{/* 最终总结(支持 Markdown + ECharts) */}
|
||||
<Box
|
||||
bg={agentBubbleBg}
|
||||
px={4}
|
||||
@@ -811,9 +802,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
||||
borderColor={borderColor}
|
||||
boxShadow="md"
|
||||
>
|
||||
<Text fontSize="sm" whiteSpace="pre-wrap">
|
||||
{message.content}
|
||||
</Text>
|
||||
<MarkdownWithCharts content={message.content} />
|
||||
|
||||
{/* 元数据 */}
|
||||
{message.metadata && (
|
||||
|
||||
Reference in New Issue
Block a user