Files
gongxue-base/apps/admin/src/components/AiChat/AiMessageContent.tsx

113 lines
3.8 KiB
TypeScript

import React from 'react';
import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import { CodeHighlighter, Think } from '@ant-design/x';
import XMarkdown from '@ant-design/x-markdown';
import type { ComponentProps } from '@ant-design/x-markdown';
import { Alert, Space, Tag, Typography } from 'antd';
import type { AiChatMessage, AiChatMessageStatus, AiToolRun } from './types';
const toolLabels: Record<string, string> = {
search_students: '查询学生',
get_student_basic: '读取学生信息',
search_classes: '查询班级',
get_attendance_summary: '统计考勤',
search_rooms: '查询房间',
get_room_occupancy_summary: '统计入住',
search_bills: '查询账单',
get_dashboard_stats: '读取经营概览',
};
const markdownComponents = {
code: ({ children, lang, block }: ComponentProps) => {
const content = String(children ?? '').replace(/\n$/, '');
if (!block) return <code>{content}</code>;
return <CodeHighlighter lang={lang || 'text'}>{content}</CodeHighlighter>;
},
};
const markdownSanitizerConfig = {
ALLOW_UNKNOWN_PROTOCOLS: false,
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'],
FORBID_ATTR: ['style'],
};
function ToolStatus({ tool }: { tool: AiToolRun }) {
const isRunning = tool.status === 'running';
const isSuccess = tool.status === 'success';
const icon = isRunning ? (
<LoadingOutlined spin />
) : isSuccess ? (
<CheckCircleOutlined />
) : (
<CloseCircleOutlined />
);
const color = isRunning ? 'processing' : isSuccess ? 'success' : 'error';
const statusText = isRunning ? '查询中' : isSuccess ? '查询完成' : tool.summary || '查询失败';
return (
<div className="ai-chat-tool" data-status={tool.status}>
<Tag icon={icon} color={color}>
{toolLabels[tool.toolName] || tool.toolName}
</Tag>
<Typography.Text type="secondary" className="ai-chat-tool__summary">
{statusText}
</Typography.Text>
</div>
);
}
export const AiMessageContent: React.FC<{
message: AiChatMessage;
status?: AiChatMessageStatus;
}> = ({ message, status }) => {
if (message.role === 'user') return <div className="ai-chat-user-text">{message.content}</div>;
const streaming = status === 'loading' || status === 'updating';
return (
<Space direction="vertical" size={10} className="ai-chat-answer">
{message.reasoningContent && (
<Think
title={streaming ? '正在思考' : '思考过程'}
loading={streaming}
defaultExpanded={false}
>
<XMarkdown
content={message.reasoningContent}
components={markdownComponents}
escapeRawHtml
openLinksInNewTab
dompurifyConfig={markdownSanitizerConfig}
streaming={{ hasNextChunk: streaming, tail: streaming }}
/>
</Think>
)}
{message.toolRuns.length > 0 && (
<div className="ai-chat-tools" aria-label="工具调用状态">
{message.toolRuns.map((tool) => (
<ToolStatus key={tool.toolCallId} tool={tool} />
))}
</div>
)}
{message.content && (
<XMarkdown
content={message.content}
components={markdownComponents}
escapeRawHtml
openLinksInNewTab
dompurifyConfig={markdownSanitizerConfig}
streaming={{
hasNextChunk: streaming,
enableAnimation: true,
tail: streaming,
incompleteMarkdownComponentMap: {
link: 'span',
image: 'span',
table: 'div',
},
}}
/>
)}
{message.error && <Alert type="error" showIcon message={message.error} />}
{message.cancelled && <Typography.Text type="secondary"></Typography.Text>}
</Space>
);
};