feat: 集成 AI 对话与只读查询工具
This commit is contained in:
112
apps/admin/src/components/AiChat/AiMessageContent.tsx
Normal file
112
apps/admin/src/components/AiChat/AiMessageContent.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user