feat(aia): Protocol Agent MVP complete with one-click generation and Word export
- Add one-click research protocol generation with streaming output - Implement Word document export via Pandoc integration - Add dynamic dual-panel layout with resizable split pane - Implement collapsible content for StatePanel stages - Add conversation history management with title auto-update - Fix scroll behavior, markdown rendering, and UI layout issues - Simplify conversation creation logic for reliability
This commit is contained in:
@@ -50,6 +50,8 @@ interface ChatAreaProps {
|
||||
conversationId?: string;
|
||||
context: ProtocolContext | null;
|
||||
onContextUpdate: () => void;
|
||||
/** 更新对话标题(首次发送消息时调用) */
|
||||
onTitleUpdate?: (title: string) => void;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -96,11 +98,15 @@ function parseExtractedData(content: string): {
|
||||
export const ChatArea: React.FC<ChatAreaProps> = ({
|
||||
conversationId,
|
||||
context,
|
||||
onContextUpdate
|
||||
onContextUpdate,
|
||||
onTitleUpdate,
|
||||
}) => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const prevConversationIdRef = useRef<string | undefined>(undefined);
|
||||
const isFirstMount = useRef(true);
|
||||
|
||||
// 使用通用 useAIStream hook 实现流式输出(打字机效果)
|
||||
const {
|
||||
@@ -130,19 +136,15 @@ export const ChatArea: React.FC<ChatAreaProps> = ({
|
||||
scrollToBottom();
|
||||
}, [messages, streamContent, scrollToBottom]);
|
||||
|
||||
// 初始化欢迎消息
|
||||
useEffect(() => {
|
||||
if (conversationId && messages.length === 0) {
|
||||
const currentStage = context?.currentStage || 'scientific_question';
|
||||
const stageName = STAGE_NAMES[currentStage] || '科学问题梳理';
|
||||
|
||||
setMessages([{
|
||||
id: 'welcome',
|
||||
role: 'assistant',
|
||||
content: `您好!我是研究方案制定助手,将帮助您系统地完成临床研究方案的核心要素设计。
|
||||
|
||||
我们将一起完成以下5个关键步骤:
|
||||
|
||||
// 生成欢迎消息(紧凑版本)
|
||||
const createWelcomeMessage = useCallback((currentStage?: string): Message => {
|
||||
const stage = currentStage || 'scientific_question';
|
||||
const stageName = STAGE_NAMES[stage] || '科学问题梳理';
|
||||
|
||||
return {
|
||||
id: 'welcome',
|
||||
role: 'assistant',
|
||||
content: `您好!我是研究方案制定助手,将帮助您系统地完成临床研究方案的核心要素设计。我们将一起完成以下5个关键步骤:
|
||||
1️⃣ **科学问题梳理** - 明确研究要解决的核心问题
|
||||
2️⃣ **PICO要素** - 确定研究人群、干预、对照和结局
|
||||
3️⃣ **研究设计** - 选择合适的研究类型和方法
|
||||
@@ -151,17 +153,77 @@ export const ChatArea: React.FC<ChatAreaProps> = ({
|
||||
|
||||
完成这5个要素后,您可以**一键生成完整的研究方案**并下载为Word文档。
|
||||
|
||||
---
|
||||
|
||||
📍 **当前阶段**: ${stageName}
|
||||
|
||||
让我们开始吧!请先告诉我,您想研究什么问题?或者描述一下您的研究背景和想法。`,
|
||||
stage: currentStage,
|
||||
stageName,
|
||||
timestamp: new Date(),
|
||||
}]);
|
||||
stage,
|
||||
stageName,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 加载历史消息(当 conversationId 变化时)
|
||||
useEffect(() => {
|
||||
// 首次挂载或 conversationId 变化时执行
|
||||
const shouldUpdate = isFirstMount.current || conversationId !== prevConversationIdRef.current;
|
||||
|
||||
if (!shouldUpdate) {
|
||||
return;
|
||||
}
|
||||
}, [conversationId, messages.length, context]);
|
||||
|
||||
isFirstMount.current = false;
|
||||
prevConversationIdRef.current = conversationId;
|
||||
|
||||
// 如果没有 conversationId,显示欢迎消息(等待自动创建会话)
|
||||
if (!conversationId) {
|
||||
setMessages([createWelcomeMessage(context?.currentStage)]);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadHistoryMessages = async () => {
|
||||
setIsLoadingHistory(true);
|
||||
try {
|
||||
const token = getAccessToken();
|
||||
const response = await fetch(`/api/v1/aia/protocol-agent/messages/${conversationId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const historyMessages = result.data?.messages || [];
|
||||
|
||||
if (historyMessages.length > 0) {
|
||||
// 有历史消息:加载历史 + 欢迎消息在最前
|
||||
const loadedMessages: Message[] = historyMessages.map((m: any) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
thinkingContent: m.thinkingContent,
|
||||
timestamp: new Date(m.createdAt),
|
||||
}));
|
||||
|
||||
// 在历史消息前添加欢迎消息
|
||||
setMessages([createWelcomeMessage(context?.currentStage), ...loadedMessages]);
|
||||
} else {
|
||||
// 没有历史消息:只显示欢迎消息
|
||||
setMessages([createWelcomeMessage(context?.currentStage)]);
|
||||
}
|
||||
} else {
|
||||
// API 失败:显示欢迎消息
|
||||
setMessages([createWelcomeMessage(context?.currentStage)]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ChatArea] 加载历史消息失败:', error);
|
||||
// 出错也显示欢迎消息
|
||||
setMessages([createWelcomeMessage(context?.currentStage)]);
|
||||
} finally {
|
||||
setIsLoadingHistory(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadHistoryMessages();
|
||||
}, [conversationId, context?.currentStage, createWelcomeMessage]);
|
||||
|
||||
// 处理流式响应完成
|
||||
useEffect(() => {
|
||||
@@ -222,6 +284,9 @@ export const ChatArea: React.FC<ChatAreaProps> = ({
|
||||
|
||||
const userContent = input.trim();
|
||||
|
||||
// 检查是否是首次用户消息(只有欢迎消息时)
|
||||
const isFirstUserMessage = messages.filter(m => m.role === 'user').length === 0;
|
||||
|
||||
// 添加用户消息
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
@@ -232,11 +297,23 @@ export const ChatArea: React.FC<ChatAreaProps> = ({
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInput('');
|
||||
|
||||
// 使用 useAIStream 发送消息(流式输出)
|
||||
// 首次消息时更新对话标题(锦上添花,不影响核心功能)
|
||||
if (isFirstUserMessage && onTitleUpdate) {
|
||||
try {
|
||||
const newTitle = userContent.length > 20
|
||||
? userContent.slice(0, 20) + '...'
|
||||
: userContent;
|
||||
onTitleUpdate(newTitle);
|
||||
} catch (e) {
|
||||
console.warn('[ChatArea] 更新标题失败,不影响对话:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
await sendStreamMessage(userContent, {
|
||||
conversationId,
|
||||
});
|
||||
}, [input, conversationId, isStreaming, sendStreamMessage]);
|
||||
}, [input, conversationId, isStreaming, sendStreamMessage, messages, onTitleUpdate]);
|
||||
|
||||
/**
|
||||
* 处理同步到方案
|
||||
@@ -321,6 +398,14 @@ export const ChatArea: React.FC<ChatAreaProps> = ({
|
||||
<section className="chat-area">
|
||||
{/* 聊天历史 */}
|
||||
<div className="chat-container" ref={chatContainerRef}>
|
||||
{/* 加载历史消息时显示加载状态 */}
|
||||
{isLoadingHistory && (
|
||||
<div className="loading-history">
|
||||
<div className="loading-spinner" />
|
||||
<span>加载历史消息...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map(msg => (
|
||||
<div key={msg.id}>
|
||||
{msg.role === 'user' && (
|
||||
|
||||
Reference in New Issue
Block a user