feat(aia): Implement Protocol Agent MVP with reusable Agent framework
Sprint 1-3 Completed (Backend + Frontend): Backend (Sprint 1-2): - Implement 5-layer Agent framework (Query->Planner->Executor->Tools->Reflection) - Create agent_schema with 6 tables (agent_definitions, stages, prompts, sessions, traces, reflexion_rules) - Create protocol_schema with 2 tables (protocol_contexts, protocol_generations) - Implement Protocol Agent core services (Orchestrator, ContextService, PromptBuilder) - Integrate LLM service adapter (DeepSeek/Qwen/GPT-5/Claude) - 6 API endpoints with full authentication - 10/10 API tests passed Frontend (Sprint 3): - Add Protocol Agent entry in AgentHub (indigo theme card) - Implement ProtocolAgentPage with 3-column layout - Collapsible sidebar (Gemini style, 48px <-> 280px) - StatePanel with 5 stage cards (scientific_question, pico, study_design, sample_size, endpoints) - ChatArea with sync button and action cards integration - 100% prototype design restoration (608 lines CSS) - Detailed endpoints structure: baseline, exposure, outcomes, confounders Features: - 5-stage dialogue flow for research protocol design - Conversation-driven interaction with sync-to-protocol button - Real-time context state management - One-click protocol generation button (UI ready, backend pending) Database: - agent_schema: 6 tables for reusable Agent framework - protocol_schema: 2 tables for Protocol Agent - Seed data: 1 agent + 5 stages + 9 prompts + 4 reflexion rules Code Stats: - Backend: 13 files, 4338 lines - Frontend: 14 files, 2071 lines - Total: 27 files, 6409 lines Status: MVP core functionality completed, pending frontend-backend integration testing Next: Sprint 4 - One-click protocol generation + Word export
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Chat Area - Protocol Agent 聊天区域
|
||||
*
|
||||
* 基于通用Chat组件,扩展Protocol Agent特有功能
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Send, Sparkles, User } from 'lucide-react';
|
||||
import type { ProtocolContext, AgentResponse } from '../types';
|
||||
import { SyncButton } from './SyncButton';
|
||||
import { ActionCardComponent } from './ActionCard';
|
||||
import { ReflexionMessage } from './ReflexionMessage';
|
||||
import { getAccessToken } from '../../../../framework/auth/api';
|
||||
|
||||
interface ChatAreaProps {
|
||||
conversationId?: string;
|
||||
context: ProtocolContext | null;
|
||||
onContextUpdate: () => void;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
thinkingContent?: string;
|
||||
syncButton?: AgentResponse['syncButton'];
|
||||
actionCards?: AgentResponse['actionCards'];
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
const API_BASE = '/api/v1/aia/protocol-agent';
|
||||
|
||||
export const ChatArea: React.FC<ChatAreaProps> = ({
|
||||
conversationId,
|
||||
context,
|
||||
onContextUpdate
|
||||
}) => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 自动滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (chatContainerRef.current) {
|
||||
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
// 初始化欢迎消息
|
||||
useEffect(() => {
|
||||
if (conversationId && messages.length === 0) {
|
||||
setMessages([{
|
||||
id: 'welcome',
|
||||
role: 'assistant',
|
||||
content: `您好!我是研究方案制定助手,将帮助您系统地完成临床研究方案的核心要素设计。
|
||||
|
||||
我们将一起完成以下5个关键步骤:
|
||||
|
||||
1️⃣ **科学问题梳理** - 明确研究要解决的核心问题
|
||||
2️⃣ **PICO要素** - 确定研究人群、干预、对照和结局
|
||||
3️⃣ **研究设计** - 选择合适的研究类型和方法
|
||||
4️⃣ **样本量计算** - 估算所需的样本量
|
||||
5️⃣ **观察指标** - 定义基线、暴露、结局指标和混杂因素
|
||||
|
||||
完成这5个要素后,您可以**一键生成完整的研究方案**并下载为Word文档。
|
||||
|
||||
让我们开始吧!请先告诉我,您想研究什么问题?或者描述一下您的研究背景和想法。`,
|
||||
timestamp: new Date(),
|
||||
}]);
|
||||
}
|
||||
}, [conversationId, messages.length]);
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || !conversationId) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: input.trim(),
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const token = getAccessToken();
|
||||
const response = await fetch(`${API_BASE}/message`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
conversationId,
|
||||
content: userMessage.content,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to send message');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.data) {
|
||||
const aiResponse: AgentResponse = result.data;
|
||||
|
||||
const aiMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: aiResponse.content,
|
||||
thinkingContent: aiResponse.thinkingContent,
|
||||
syncButton: aiResponse.syncButton,
|
||||
actionCards: aiResponse.actionCards,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
|
||||
// 刷新上下文状态
|
||||
onContextUpdate();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ChatArea] handleSend error:', err);
|
||||
// 显示错误消息
|
||||
setMessages(prev => [...prev, {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'system',
|
||||
content: '抱歉,消息发送失败。请稍后重试。',
|
||||
timestamp: new Date(),
|
||||
}]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理同步
|
||||
*/
|
||||
const handleSync = async (stageCode: string, data: Record<string, unknown>) => {
|
||||
if (!conversationId) return;
|
||||
|
||||
try {
|
||||
const token = getAccessToken();
|
||||
const response = await fetch(`${API_BASE}/sync`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
conversationId,
|
||||
stageCode,
|
||||
data,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to sync data');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// 添加系统消息
|
||||
const systemMsg: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'system',
|
||||
content: result.data.message || '✅ 已同步到方案',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages(prev => [...prev, systemMsg]);
|
||||
|
||||
// 刷新上下文
|
||||
onContextUpdate();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ChatArea] handleSync error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 按Enter发送
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="chat-area">
|
||||
{/* 聊天历史 */}
|
||||
<div className="chat-container" ref={chatContainerRef}>
|
||||
{messages.map(msg => (
|
||||
<div key={msg.id}>
|
||||
{msg.role === 'user' && (
|
||||
<div className="message-row user-row">
|
||||
<div className="avatar user-avatar">
|
||||
<User size={16} />
|
||||
</div>
|
||||
<div className="message-content">
|
||||
<div className="message-meta">User • {formatTime(msg.timestamp)}</div>
|
||||
<div className="chat-bubble user-bubble">
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg.role === 'assistant' && (
|
||||
<div className="message-row assistant-row">
|
||||
<div className="avatar assistant-avatar">
|
||||
<Sparkles size={16} />
|
||||
</div>
|
||||
<div className="message-content">
|
||||
<div className="message-meta">Protocol Agent • {formatTime(msg.timestamp)}</div>
|
||||
<div className="chat-bubble assistant-bubble">
|
||||
{msg.content}
|
||||
</div>
|
||||
|
||||
{/* 同步按钮 */}
|
||||
{msg.syncButton && (
|
||||
<SyncButton
|
||||
syncData={msg.syncButton}
|
||||
onSync={handleSync}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Action Cards */}
|
||||
{msg.actionCards && msg.actionCards.length > 0 && (
|
||||
<div className="action-cards">
|
||||
{msg.actionCards.map(card => (
|
||||
<ActionCardComponent key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg.role === 'system' && (
|
||||
<div className="message-row system-row">
|
||||
<div className="system-message">
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{loading && (
|
||||
<div className="message-row assistant-row">
|
||||
<div className="avatar assistant-avatar">
|
||||
<Sparkles size={16} />
|
||||
</div>
|
||||
<div className="message-content">
|
||||
<div className="chat-bubble assistant-bubble thinking">
|
||||
<span className="thinking-dot"></span>
|
||||
<span className="thinking-dot"></span>
|
||||
<span className="thinking-dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 输入区 */}
|
||||
<div className="input-area">
|
||||
<div className="input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
placeholder="输入您的指令..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={loading || !conversationId}
|
||||
/>
|
||||
<button
|
||||
className="send-btn"
|
||||
onClick={handleSend}
|
||||
disabled={loading || !input.trim() || !conversationId}
|
||||
>
|
||||
<Send size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user