import React, { useMemo, useState } from 'react'; import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined, TableOutlined, } from '@ant-design/icons'; import FileCard from '@ant-design/x/es/file-card'; import Sources from '@ant-design/x/es/sources'; import Think from '@ant-design/x/es/think'; import ThoughtChain from '@ant-design/x/es/thought-chain'; import type { ThoughtChainItemType } from '@ant-design/x'; import XMarkdown, { type ComponentProps } from '@ant-design/x-markdown'; import { Alert, Button, Flex, Input, Space, Typography } from 'antd'; import { useUserStore } from '../../store/user/userStore'; import { DynamicChart } from './DynamicChart'; import { DynamicForm } from './DynamicForm'; import { DynamicReview } from './DynamicReview'; import { LiteCodeHighlighter } from './LiteCodeHighlighter'; import { LiteMermaid } from './LiteMermaid'; import type { AiAttachment, AiChatMessage, AiChatMessageStatus, AiChartSchema, AiFormSchema, AiImportWizard, AiReviewSection, AiReviewSchema, AiReviewSectionType, AiToolRun, } from './types'; const toolLabels: Record = { search_students: '查询学生', get_student_basic: '读取学生信息', search_classes: '查询班级', get_attendance_summary: '统计考勤', search_rooms: '查询房间', get_room_occupancy_summary: '统计入住', search_bills: '查询账单', get_dashboard_stats: '读取经营概览', render_form: '生成表单', render_review: '生成导入预览', render_chart: '生成图表', start_import_wizard: '生成导入向导', create_student: '创建学生', search_exams: '查询考试', search_schedules: '查询课表', search_deposits: '查询押金', search_expenses: '查询费用', search_classrooms: '查询教室', search_classroom_rentals: '查询教室租用', get_sync_status: '查询同步状态', get_business_context: '读取业务流程', get_entity_schema: '读取实体字典', get_pending_tasks: '查询业务待办', }; const markdownComponents = { code: ({ children, lang, block }: ComponentProps) => { const content = String(children ?? '').replace(/\n$/, ''); if (!block) return {content}; if (lang === 'mermaid') return {content}; return {content}; }, }; const markdownSanitizerConfig = { ALLOW_UNKNOWN_PROTOCOLS: false, FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'], FORBID_ATTR: ['style'], }; function attachmentIcon(attachment: AiAttachment) { if (attachment.mimeType === 'application/pdf') return 'pdf' as const; if (attachment.mimeType.includes('wordprocessingml')) return 'word' as const; if (attachment.mimeType.includes('spreadsheetml')) return 'excel' as const; if (attachment.mimeType.startsWith('image/')) return 'image' as const; return 'default' as const; } async function openAttachment(attachment: AiAttachment): Promise { const token = useUserStore.getState().token; const response = await fetch(attachment.url, { headers: token ? { Authorization: `Bearer ${token}` } : undefined, }); if (!response.ok) throw new Error('附件打开失败'); const objectUrl = URL.createObjectURL(await response.blob()); window.open(objectUrl, '_blank', 'noopener,noreferrer'); window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000); } async function openSourceUrl(item: { url?: string }): Promise { if (!item.url) return; const token = useUserStore.getState().token; const response = await fetch(item.url, { headers: token ? { Authorization: `Bearer ${token}` } : undefined, }); if (!response.ok) throw new Error('来源打开失败'); const objectUrl = URL.createObjectURL(await response.blob()); window.open(objectUrl, '_blank', 'noopener,noreferrer'); window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000); } function ToolChain({ tools }: { tools: AiToolRun[] }) { const items = useMemo( () => tools.map((tool) => { const running = tool.status === 'running'; const success = tool.status === 'success'; return { key: tool.toolCallId, title: toolLabels[tool.toolName] || tool.toolName, description: tool.durationMs ? `${tool.durationMs}ms` : undefined, content: tool.summary || (running ? '正在查询业务数据' : success ? '查询完成' : '查询失败'), status: running ? 'loading' : success ? 'success' : 'error', icon: running ? ( ) : success ? ( ) : ( ), collapsible: Boolean(tool.summary), }; }), [tools], ); return ; } function EditUserContent({ initial, onConfirm, onCancel, }: { initial: string; onConfirm: (value: string) => void; onCancel?: () => void; }) { const [draft, setDraft] = useState(initial); return ( setDraft(event.target.value)} autoSize={{ minRows: 2, maxRows: 8 }} onKeyDown={(event) => { // 中文输入法合成中的回车不应触发保存 if (event.nativeEvent.isComposing || event.keyCode === 229) return; if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); onConfirm(draft); } else if (event.key === 'Escape') { onCancel?.(); } }} /> ); } export interface AiMessageContentProps { message: AiChatMessage; status?: AiChatMessageStatus; editing?: boolean; onEditConfirm?: (value: string) => void; onEditCancel?: () => void; onSubmitForm?: (form: AiFormSchema, values: Record) => void; onSubmitReview?: (reviewId: string, reviewTitle?: string) => void; onConfirmReviewStep?: ( messageId: number | undefined, reviewId: string, sectionKey: AiReviewSection['key'], ) => AiReviewSchema | Promise | void; onConfirmReviewGroup?: ( messageId: number | undefined, reviewId: string, type: AiReviewSectionType, ) => AiReviewSchema | Promise | void; onOpenImportWizard?: (runId: string) => void; } export const AiMessageContent: React.FC = ({ message, status, editing, onEditConfirm, onEditCancel, onSubmitForm, onSubmitReview, onConfirmReviewStep, onConfirmReviewGroup, onOpenImportWizard, }) => { const streaming = status === 'loading' || status === 'updating'; const formSubmission = message.metadata?.a2uiSubmit; const reviewSubmission = message.metadata?.a2uiReviewSubmit; const sourceMeta = message.metadata?.a2uiSources; const sourceItems = Array.isArray(sourceMeta) ? sourceMeta .filter( (item): item is { title: string; url?: string; description?: string } => Boolean(item) && typeof (item as { title?: unknown }).title === 'string', ) .map((item, index) => ({ key: `source-${index}`, title: item.title, ...(item.url ? { url: item.url } : {}), ...(item.description ? { description: item.description } : {}), })) : []; const attachmentCards = message.attachments.map((attachment) => ( void openAttachment(attachment)} /> )); if (message.role === 'user') { if (reviewSubmission && typeof reviewSubmission === 'object') { const reviewTitle = typeof (reviewSubmission as Record).reviewTitle === 'string' ? String((reviewSubmission as Record).reviewTitle) : '批量导入'; return ( ); } if (formSubmission && typeof formSubmission === 'object') { const formTitle = typeof (formSubmission as Record).formTitle === 'string' ? String((formSubmission as Record).formTitle) : '表单'; return ( ); } return ( {attachmentCards.length > 0 && ( {attachmentCards} )} {editing ? ( onEditConfirm?.(value)} onCancel={onEditCancel} /> ) : (
{message.content}
)}
); } return ( {streaming && !message.content && !message.reasoningContent && message.toolRuns.length === 0 && (
)} {message.retrying && ( )} {message.reasoningContent && ( )} {message.toolRuns.length > 0 && } {attachmentCards.length > 0 && ( {attachmentCards} )} {(() => { const wizard = message.metadata?.a2uiImportWizard as AiImportWizard | undefined; if (!wizard || !onOpenImportWizard) return null; return ( {wizard.fileName} ); })()} {message.content && ( )} {sourceItems.length > 0 && ( void openSourceUrl(item as { url?: string })} /> )} {(message.forms ?? []).map((form) => ( onSubmitForm?.(form, values)} /> ))} {(message.reviews ?? []).map((review: AiReviewSchema) => ( onSubmitReview?.(reviewId, review.title)} onConfirmStep={onConfirmReviewStep} onConfirmGroup={onConfirmReviewGroup} /> ))} {(message.charts ?? []).map((chart: AiChartSchema) => ( ))} {message.error && } {message.cancelled && 回答已停止}
); };