- 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)
184 lines
5.2 KiB
TypeScript
184 lines
5.2 KiB
TypeScript
/**
|
||
* 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;
|