import React, { useEffect } from 'react'; import './style.less'; import ReactMarkdown from 'react-markdown'; import { createFromIconfontCN, LoadingOutlined } from '@ant-design/icons'; import { Spin, Tooltip } from 'antd'; import { Column, Line, Pie } from '@ant-design/charts'; export interface Message { id?: string; // 创建消息时赋予唯一 id sender: 'user' | 'bot'; thinkContent?: string; content: string; // 可能包含纯文本或 HTML loading?: boolean; duration?: number; // 回答耗时(秒) collapse?: boolean; // 保存折叠状态 complete?: boolean; contentType?: string; charts?: { type: 'LineChart' | 'ColumnChart' | 'PieChart'; xField: string; yField: string; angleField: string; colorField: string; data: any[]; }; } const IconFont = createFromIconfontCN({ scriptUrl: '', // 请填写图标脚本地址 }); const antIcon = ; // 单独封装一个 memoized 的 Spinner 组件,避免频繁重绘导致动画重置 const SpinnerComponent: React.FC<{ loading: boolean }> = React.memo(({ loading }) => { return loading ? : null; }); // 拆分机器人消息中的状态部分(头像旁边那块) // 新增属性 hasDeepThinking,根据该属性显示不同的文案 interface BotMessageStatusProps { loading: boolean; duration?: number; collapse: boolean; onToggleCollapse: () => void; hasDeepThinking: boolean; } const BotMessageStatus: React.FC = React.memo( ({ loading, duration, collapse, onToggleCollapse, hasDeepThinking }) => { return (
{loading ? ( <> {hasDeepThinking ? '深度思考中…' : '内容输出中…'} ) : ( <> {hasDeepThinking ? `已深度思考${duration ? `(用时${duration}秒)` : ''}` : `内容已输出完毕${duration ? `(用时${duration}秒)` : ''}`} {hasDeepThinking && } )}
); }, (prevProps, nextProps) => prevProps.loading === nextProps.loading && prevProps.duration === nextProps.duration && prevProps.collapse === nextProps.collapse && prevProps.hasDeepThinking === nextProps.hasDeepThinking, ); // 拆分机器人消息中的文本内容部分,同时处理图表展示 interface BotMessageContentProps { thinkContent?: string; content: string; msg: Message; charts?: { type: string; xField: string; yField: string; angleField: string; colorField: string; data: any[]; }; // 新增 onShowDetail 回调属性,用于点击详情按钮时触发 onShowDetail?: (charts: any, content: string) => void; } const BotMessageContent: React.FC = React.memo(({ thinkContent, content, charts, onShowDetail, msg }) => { return (
{thinkContent}
{/* 判断是否存在图表数据 */} {charts && charts.data && charts.data.length > 0 && (
0 && msg.complete ? 'charts-content canReadDetail' : 'charts-content'}>
onShowDetail && onShowDetail(charts, content)}> 详情
{(charts.type === 'LineChart' && charts) && } {charts.type === 'ColumnChart' && } {charts.type === 'PieChart' && }
)} {content}
); }); // 整体的机器人消息组件 interface BotMessageProps { msg: Message; onToggleCollapse: () => void; handleCopy: (text: string) => void; ifCopied: boolean; // 新增 onShowDetail 回调属性 onShowDetail?: (charts: any, content: string) => void; } const BotMessage: React.FC = ({ msg, onToggleCollapse, handleCopy, ifCopied, onShowDetail }) => { const isCollapsed = msg.collapse || false; // 通过判断 thinkContent 是否存在且非空来确定是否存在深度思考内容 const hasDeepThinking = !!(msg.thinkContent && msg.thinkContent.trim().length > 0); return (
{msg.content.length > 0 && msg.complete && (
handleCopy(msg.content)}>
)}
); }; interface ChatProps { messages: Message[]; chatContainerRef: React.RefObject; setMessages: React.Dispatch>; // 新增 onShowDetail 回调属性,用于将详情数据传递到上层组件(例如 AiChat) onShowDetail?: (charts: any, content: string) => void; } const Chat: React.FC = ({ messages, chatContainerRef, setMessages, onShowDetail }) => { const [ifCopied, setIfCopied] = React.useState(false); const fallbackCopyTextToClipboard = (text: string) => { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.top = '0'; textArea.style.left = '0'; textArea.style.width = '2em'; textArea.style.height = '2em'; textArea.style.padding = '0'; textArea.style.border = 'none'; textArea.style.outline = 'none'; textArea.style.boxShadow = 'none'; textArea.style.background = 'transparent'; document.body.appendChild(textArea); textArea.select(); try { const successful = document.execCommand('copy'); if (successful) setIfCopied(true); } catch (err) { console.error('复制失败', err); } document.body.removeChild(textArea); }; const handleCopy = (text: string) => { if (!text) { console.error('未获取到文本内容'); return; } if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard .writeText(text) .then(() => { setIfCopied(true); }) .catch((err) => { console.error('使用 Clipboard API 复制失败,尝试备用方案:', err); fallbackCopyTextToClipboard(text); }); } else { fallbackCopyTextToClipboard(text); } }; useEffect(() => { if (ifCopied) { const timer = setTimeout(() => { setIfCopied(false); }, 2000); return () => clearTimeout(timer); } }, [ifCopied]); return (
{messages.map((msg, idx) => { const key = msg.id || idx; if (msg.sender === 'user') { return (
{msg.content}
); } else { return (
{ setMessages((prevMessages) => prevMessages.map((m, i) => (i === idx ? { ...m, collapse: !m.collapse } : m))); }} handleCopy={handleCopy} ifCopied={ifCopied} onShowDetail={onShowDetail} />
); } })}
); }; export default Chat;