Files
AIclinicalresearch/frontend-v2/src/shared/components/Chat/ChatContainer.tsx
HaHafeng 06028c6952 feat(pkb): implement complete batch processing workflow and frontend optimization
- Frontend V3 architecture migration to modules/pkb
- Implement three work modes: full-text reading, deep reading, batch processing
- Complete batch processing: template selection, progress display, result export (CSV)
- Integrate Ant Design X Chat component with streaming support
- Add document upload modal with drag-and-drop support
- Optimize UI: multi-line table display, citation formatting, auto-scroll
- Fix 10+ technical issues: API mapping, state sync, form clearing
- Update documentation: development records and module status

Performance: 3 docs batch processing ~17-28s
Status: PKB module now production-ready (90% complete)
2026-01-07 18:23:43 +08:00

184 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ChatContainer - 通用 AI 对话容器组件
*
* 基于 Ant Design X + X SDK 构建
* 支持多场景AIA、PKB、Tool C
*
* 注意:由于 Ant Design X SDK 的复杂性,这里采用简化实现
* 直接管理消息状态,不使用 useXChat Hook
*/
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Bubble, Sender } from '@ant-design/x';
import type { ChatContainerProps, ChatMessage } from './types';
import type { BubbleItemType } from '@ant-design/x/es/bubble/interface';
import './styles/chat.css';
/**
* ChatContainer 组件(简化实现)
*/
export const ChatContainer: React.FC<ChatContainerProps> = ({
conversationKey: _conversationKey,
defaultMessages = [],
providerConfig,
customMessageRenderer,
senderProps = {},
onMessageSent,
onMessageReceived,
onError,
className = '',
style = {},
}) => {
// 如果没有默认消息,添加欢迎语
const initialMessages = defaultMessages.length > 0 ? defaultMessages : [{
id: 'welcome',
role: 'assistant' as const,
content: '您好!我是您的 AI 数据分析师。我可以帮您编写代码来清洗数据。试试说:"把年龄大于60的设为老年组"。',
status: 'success' as const,
timestamp: Date.now(),
}];
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
const [isLoading, setIsLoading] = useState(false);
const [inputValue, setInputValue] = useState(''); // 受控输入框
const messagesEndRef = useRef<HTMLDivElement>(null); // 用于滚动到底部
// 滚动到底部
const scrollToBottom = useCallback(() => {
setTimeout(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, 100);
}, []);
// 消息变化时自动滚动
useEffect(() => {
scrollToBottom();
}, [messages, scrollToBottom]);
// 处理消息发送
const handleSend = useCallback(async (messageContent: string) => {
if (!messageContent.trim()) return; // 防止发送空消息
// 🔑 立即清除输入框
setInputValue('');
// 1. 添加用户消息
const userMessage: ChatMessage = {
id: Date.now(),
role: 'user',
content: messageContent,
status: 'success',
timestamp: Date.now(),
};
setMessages(prev => [...prev, userMessage]);
if (onMessageSent) {
onMessageSent(userMessage);
}
// 2. 显示加载状态
const loadingMessage: ChatMessage = {
id: 'loading',
role: 'assistant',
content: '正在思考...',
status: 'loading',
timestamp: Date.now(),
};
setMessages(prev => [...prev, loadingMessage]);
setIsLoading(true);
try {
// 3. 调用后端 API
let response;
if (providerConfig.requestFn) {
response = await providerConfig.requestFn(messageContent, { messages });
} else {
const res = await fetch(providerConfig.apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: messageContent }),
});
if (!res.ok) throw new Error('API 请求失败');
response = await res.json();
}
// 4. 移除加载消息,添加 AI 响应
const aiMessage: ChatMessage = {
id: response.messageId || Date.now(),
role: 'assistant',
content: response.explanation || response.message || response.content || '',
status: 'success',
code: response.code,
explanation: response.explanation,
timestamp: Date.now(),
metadata: response.metadata,
};
setMessages(prev => prev.filter(m => m.id !== 'loading').concat(aiMessage));
if (onMessageReceived) {
onMessageReceived(aiMessage);
}
} catch (error: any) {
// 5. 错误处理
const errorMessage: ChatMessage = {
id: Date.now(),
role: 'assistant',
content: `抱歉,处理您的请求时出现错误:${error.message}`,
status: 'error',
timestamp: Date.now(),
};
setMessages(prev => prev.filter(m => m.id !== 'loading').concat(errorMessage));
if (onError) {
onError(error);
}
} finally {
setIsLoading(false);
}
}, [messages, providerConfig, onMessageSent, onMessageReceived, onError]);
// 转换消息为 Bubble.List 所需格式
const bubbleItems: BubbleItemType[] = messages.map((msg) => ({
key: msg.id,
role: msg.role,
content: customMessageRenderer
? customMessageRenderer({ id: msg.id, message: msg, status: msg.status || 'success' })
: msg.content,
status: msg.status,
}));
return (
<div className={`chat-container ${className}`} style={style}>
{/* 消息列表 */}
<div className="chat-messages">
<Bubble.List
items={bubbleItems}
autoScroll={true}
/>
{/* 滚动锚点 */}
<div ref={messagesEndRef} />
</div>
{/* 输入框(受控模式) */}
<div className="chat-input">
<Sender
placeholder="输入消息..."
loading={isLoading}
value={inputValue}
onChange={setInputValue}
onSubmit={handleSend}
{...senderProps}
/>
</div>
</div>
);
};
export default ChatContainer;