From 9c5900c7f577423a32b92782e6ac9acc8bd5d6a0 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 28 Nov 2025 12:27:30 +0800 Subject: [PATCH] update pay function --- src/components/ChatBot/EChartsRenderer.js | 19 +- src/components/ChatBot/MarkdownWithCharts.js | 24 +- .../ChatArea/ExecutionStepsDisplay.js | 362 +++++++++++++++--- .../components/ChatArea/MessageRenderer.js | 19 +- src/views/AgentChat/hooks/useAgentChat.ts | 2 +- src/views/AgentChat/hooks/useAgentSessions.ts | 2 +- 6 files changed, 363 insertions(+), 65 deletions(-) diff --git a/src/components/ChatBot/EChartsRenderer.js b/src/components/ChatBot/EChartsRenderer.js index 396b4ef5..e0bf3feb 100644 --- a/src/components/ChatBot/EChartsRenderer.js +++ b/src/components/ChatBot/EChartsRenderer.js @@ -14,15 +14,27 @@ export const EChartsRenderer = ({ option, height = 400 }) => { const chartRef = useRef(null); const chartInstance = useRef(null); const bgColor = useColorModeValue('white', 'gray.800'); + const isDarkMode = useColorModeValue(false, true); useEffect(() => { if (!chartRef.current || !option) return; - // 初始化图表 + // 初始化图表(支持深色模式) if (!chartInstance.current) { - chartInstance.current = echarts.init(chartRef.current); + chartInstance.current = echarts.init(chartRef.current, isDarkMode ? 'dark' : null); } + // 深色模式下的默认文字颜色 + const darkModeTextStyle = isDarkMode + ? { + textStyle: { color: '#e5e7eb' }, + title: { textStyle: { color: '#f3f4f6' }, ...option?.title }, + legend: { textStyle: { color: '#d1d5db' }, ...option?.legend }, + xAxis: { axisLabel: { color: '#9ca3af' }, axisLine: { lineStyle: { color: '#4b5563' } }, ...option?.xAxis }, + yAxis: { axisLabel: { color: '#9ca3af' }, axisLine: { lineStyle: { color: '#4b5563' } }, splitLine: { lineStyle: { color: '#374151' } }, ...option?.yAxis }, + } + : {}; + // 设置默认主题配置 const defaultOption = { backgroundColor: 'transparent', @@ -33,6 +45,7 @@ export const EChartsRenderer = ({ option, height = 400 }) => { containLabel: true, }, ...option, + ...darkModeTextStyle, }; // 设置图表配置 @@ -49,7 +62,7 @@ export const EChartsRenderer = ({ option, height = 400 }) => { window.removeEventListener('resize', handleResize); // chartInstance.current?.dispose(); // 不要销毁,避免重新渲染时闪烁 }; - }, [option]); + }, [option, isDarkMode]); // 组件卸载时销毁图表 useEffect(() => { diff --git a/src/components/ChatBot/MarkdownWithCharts.js b/src/components/ChatBot/MarkdownWithCharts.js index 609c14f2..44b4a9b0 100644 --- a/src/components/ChatBot/MarkdownWithCharts.js +++ b/src/components/ChatBot/MarkdownWithCharts.js @@ -2,7 +2,7 @@ // 支持 ECharts 图表的 Markdown 渲染组件 import React from 'react'; -import { Box, Alert, AlertIcon, Text, VStack, Code } from '@chakra-ui/react'; +import { Box, Alert, AlertIcon, Text, VStack, Code, useColorModeValue } from '@chakra-ui/react'; import ReactMarkdown from 'react-markdown'; import { EChartsRenderer } from './EChartsRenderer'; import { logger } from '@utils/logger'; @@ -59,6 +59,12 @@ const parseMarkdownWithCharts = (markdown) => { export const MarkdownWithCharts = ({ content }) => { const parts = parseMarkdownWithCharts(content); + // 深色/浅色模式颜色 + const textColor = useColorModeValue('gray.700', 'inherit'); + const headingColor = useColorModeValue('gray.800', 'inherit'); + const blockquoteColor = useColorModeValue('gray.600', 'gray.300'); + const codeBg = useColorModeValue('gray.100', 'rgba(255, 255, 255, 0.1)'); + return ( {parts.map((part, index) => { @@ -70,22 +76,22 @@ export const MarkdownWithCharts = ({ content }) => { components={{ // 自定义渲染样式 p: ({ children }) => ( - + {children} ), h1: ({ children }) => ( - + {children} ), h2: ({ children }) => ( - + {children} ), h3: ({ children }) => ( - + {children} ), @@ -100,17 +106,17 @@ export const MarkdownWithCharts = ({ content }) => { ), li: ({ children }) => ( - + {children} ), code: ({ inline, children }) => inline ? ( - + {children} ) : ( - + {children} ), @@ -121,7 +127,7 @@ export const MarkdownWithCharts = ({ content }) => { pl={4} py={2} fontStyle="italic" - color="gray.600" + color={blockquoteColor} > {children} diff --git a/src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js b/src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js index 6b9321c2..30c9678d 100644 --- a/src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js +++ b/src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js @@ -1,8 +1,8 @@ // src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js // 执行步骤显示组件 -import React from 'react'; -import { motion } from 'framer-motion'; +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; import { Accordion, AccordionItem, @@ -16,8 +16,318 @@ import { VStack, Flex, Text, + Box, + Code, + IconButton, + Tooltip, + Collapse, } from '@chakra-ui/react'; -import { Activity } from 'lucide-react'; +import { Activity, ChevronDown, ChevronRight, Copy, Check, Database, FileJson } from 'lucide-react'; +import { MarkdownWithCharts } from '@components/ChatBot/MarkdownWithCharts'; + +/** + * 格式化结果数据用于显示 + */ +const formatResultData = (data) => { + if (data === null || data === undefined) return null; + if (typeof data === 'string') return data; + try { + return JSON.stringify(data, null, 2); + } catch { + return String(data); + } +}; + +/** + * 获取结果数据的预览文本 + */ +const getResultPreview = (result) => { + if (!result) return '无数据'; + + // 如果有 data 字段 + if (result.data) { + const data = result.data; + // 检查常见的数据结构 + if (data.chart_data) { + return `图表数据: ${data.chart_data.labels?.length || 0} 项`; + } + if (data.sector_data) { + const sectorCount = Object.keys(data.sector_data).length; + return `${sectorCount} 个板块分析`; + } + if (data.stocks) { + return `${data.stocks.length} 只股票`; + } + if (Array.isArray(data)) { + return `${data.length} 条记录`; + } + if (data.date || data.formatted_date) { + return `日期: ${data.formatted_date || data.date}`; + } + } + + // 如果结果本身是数组 + if (Array.isArray(result)) { + return `${result.length} 条记录`; + } + + // 如果是对象,返回键数量 + if (typeof result === 'object') { + const keys = Object.keys(result); + return `${keys.length} 个字段`; + } + + return '查看详情'; +}; + +/** + * 单个步骤卡片组件 + */ +const StepCard = ({ result, idx }) => { + const [isExpanded, setIsExpanded] = useState(false); + const [copied, setCopied] = useState(false); + + const hasResult = result.result && ( + typeof result.result === 'object' + ? Object.keys(result.result).length > 0 + : result.result + ); + + const handleCopy = async (e) => { + e.stopPropagation(); + try { + await navigator.clipboard.writeText(formatResultData(result.result)); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('复制失败:', err); + } + }; + + // 渲染结果数据 + const renderResultData = () => { + if (!result.result) return null; + + const data = result.result; + + // 如果有 echarts 图表数据,尝试生成图表 + if (data.data?.chart_data) { + const chartData = data.data.chart_data; + const echartsConfig = { + title: { text: `${data.data.formatted_date || ''} 涨停概念分布` }, + tooltip: { trigger: 'axis' }, + xAxis: { + type: 'category', + data: chartData.labels || [], + axisLabel: { rotate: 30, fontSize: 10 }, + }, + yAxis: { type: 'value' }, + series: [ + { + name: '涨停家数', + type: 'bar', + data: chartData.counts || [], + itemStyle: { + color: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: '#ff7043' }, + { offset: 1, color: '#ff5722' }, + ], + }, + }, + }, + ], + }; + + const markdownContent = `\`\`\`echarts +${JSON.stringify(echartsConfig)} +\`\`\``; + + return ( + + + + {/* 板块详情 */} + {data.data?.sector_data && ( + + + 板块详情 ({Object.keys(data.data.sector_data).length} 个板块) + + + {Object.entries(data.data.sector_data).map(([sector, info]) => ( + + + {sector} + {info.count} 只 + + {info.stocks?.slice(0, 3).map((stock, i) => ( + + + {stock.sname} ({stock.scode}) + + {stock.brief && ( + + {stock.brief.replace(/
/g, ' ')} +
+ )} +
+ ))} + {info.stocks?.length > 3 && ( + + 还有 {info.stocks.length - 3} 只... + + )} +
+ ))} +
+
+ )} +
+ ); + } + + // 默认显示 JSON 数据 + return ( + + + {formatResultData(data)} + + + ); + }; + + return ( + + + + {/* 步骤头部 - 可点击展开 */} + hasResult && setIsExpanded(!isExpanded)} + > + + {hasResult && ( + + + + )} + + 步骤 {idx + 1}: {result.tool_name || result.tool} + + + + + {hasResult && ( + + : } + onClick={handleCopy} + color={copied ? 'green.400' : 'gray.500'} + _hover={{ bg: 'rgba(255, 255, 255, 0.1)' }} + aria-label="复制" + /> + + )} + + {result.status} + + + + + {/* 步骤元信息 */} + + {result.execution_time && ( + ⏱️ {result.execution_time.toFixed(2)}s + )} + {hasResult && ( + + + {getResultPreview(result.result)} + + )} + + + {/* 错误信息 */} + {result.error && ( + + ⚠️ {result.error} + + )} + + {/* 展开的详细数据 */} + + {isExpanded && renderResultData()} + + + + + ); +}; /** * ExecutionStepsDisplay - 执行步骤显示组件 @@ -61,51 +371,7 @@ const ExecutionStepsDisplay = ({ steps, plan }) => { {steps.map((result, idx) => ( - - - - - - 步骤 {idx + 1}: {result.tool_name} - - - {result.status} - - - - {result.execution_time?.toFixed(2)}s - - {result.error && ( - - ⚠️ {result.error} - - )} - - - + ))} diff --git a/src/views/AgentChat/components/ChatArea/MessageRenderer.js b/src/views/AgentChat/components/ChatArea/MessageRenderer.js index f056b9f3..10d3afce 100644 --- a/src/views/AgentChat/components/ChatArea/MessageRenderer.js +++ b/src/views/AgentChat/components/ChatArea/MessageRenderer.js @@ -19,6 +19,7 @@ import { import { Cpu, User, Copy, ThumbsUp, ThumbsDown, File } from 'lucide-react'; import { MessageTypes } from '../../constants/messageTypes'; import ExecutionStepsDisplay from './ExecutionStepsDisplay'; +import { MarkdownWithCharts } from '@components/ChatBot/MarkdownWithCharts'; /** * MessageRenderer - 消息渲染器组件 @@ -139,9 +140,21 @@ const MessageRenderer = ({ message, userAvatar }) => { boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)" > - - {message.content} - + + + {message.stepResults && message.stepResults.length > 0 && ( diff --git a/src/views/AgentChat/hooks/useAgentChat.ts b/src/views/AgentChat/hooks/useAgentChat.ts index 303f9692..7fb01dc6 100644 --- a/src/views/AgentChat/hooks/useAgentChat.ts +++ b/src/views/AgentChat/hooks/useAgentChat.ts @@ -164,7 +164,7 @@ export const useAgentChat = ({ isUser: m.type === MessageTypes.USER, content: m.content, })), - user_id: user?.id || 'anonymous', + user_id: user?.id ? String(user.id) : 'anonymous', user_nickname: user?.nickname || '匿名用户', user_avatar: user?.avatar || '', subscription_type: user?.subscription_type || 'free', diff --git a/src/views/AgentChat/hooks/useAgentSessions.ts b/src/views/AgentChat/hooks/useAgentSessions.ts index 56dbc4d1..d5996b22 100644 --- a/src/views/AgentChat/hooks/useAgentSessions.ts +++ b/src/views/AgentChat/hooks/useAgentSessions.ts @@ -95,7 +95,7 @@ export const useAgentSessions = ({ setIsLoadingSessions(true); try { const response = await axios.get('/mcp/agent/sessions', { - params: { user_id: user.id, limit: 50 }, + params: { user_id: String(user.id), limit: 50 }, }); if (response.data.success) {