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:
2026-01-24 17:29:24 +08:00
parent 61cdc97eeb
commit 96290d2f76
345 changed files with 13945 additions and 47 deletions

View File

@@ -0,0 +1,245 @@
/**
* Protocol Agent 页面
*
* 100%还原原型图0119的精致设计
* 三栏布局:可折叠侧边栏 + 聊天区 + 状态面板
*/
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import {
Bot, Settings, ChevronLeft, Menu, Plus,
MessageSquare, Trash2
} from 'lucide-react';
import { ChatArea } from './components/ChatArea';
import { StatePanel } from './components/StatePanel';
import { useProtocolContext } from './hooks/useProtocolContext';
import { useProtocolConversations } from './hooks/useProtocolConversations';
import './styles/protocol-agent.css';
export const ProtocolAgentPage: React.FC = () => {
const navigate = useNavigate();
const { conversationId } = useParams<{ conversationId?: string }>();
// 侧边栏折叠状态
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
// 会话管理
const {
conversations,
currentConversation,
createConversation,
selectConversation,
deleteConversation,
} = useProtocolConversations(conversationId);
// 上下文状态
const { context, refreshContext } = useProtocolContext(currentConversation?.id);
// 首次进入且无conversationId时自动创建新对话
const [isCreating, setIsCreating] = useState(false);
useEffect(() => {
if (!conversationId && !currentConversation && !isCreating) {
console.log('[ProtocolAgentPage] 自动创建新对话...');
setIsCreating(true);
createConversation().then(newConv => {
if (newConv) {
console.log('[ProtocolAgentPage] 新对话创建成功:', newConv.id);
navigate(`/aia/protocol-agent/${newConv.id}`, { replace: true });
} else {
console.error('[ProtocolAgentPage] 创建对话失败');
setIsCreating(false);
}
}).catch(err => {
console.error('[ProtocolAgentPage] 创建对话异常:', err);
setIsCreating(false);
});
}
}, [conversationId, currentConversation, isCreating, createConversation, navigate]);
// 获取当前阶段信息
const currentStageName = context?.stageName || '科学问题梳理';
const currentStageIndex = context?.stages?.findIndex(s => s.status === 'current') ?? 0;
// 创建新对话
const handleNewConversation = async () => {
const newConv = await createConversation();
if (newConv) {
navigate(`/aia/protocol-agent/${newConv.id}`);
setSidebarCollapsed(true);
}
};
// 选择对话
const handleSelectConversation = (id: string) => {
selectConversation(id);
navigate(`/aia/protocol-agent/${id}`);
setSidebarCollapsed(true);
};
// 返回AgentHub
const handleBack = () => {
navigate('/aia');
};
// 加载状态
if (isCreating || (!conversationId && !currentConversation)) {
return (
<div className="protocol-agent-page">
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
flexDirection: 'column',
gap: '16px'
}}>
<div style={{
width: '40px',
height: '40px',
border: '4px solid #E5E7EB',
borderTopColor: '#6366F1',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<p style={{ color: '#6B7280', fontSize: '14px' }}>...</p>
</div>
</div>
);
}
return (
<div className="protocol-agent-page">
{/* 可折叠侧边栏 - Gemini风格 */}
<aside className={`sidebar ${sidebarCollapsed ? 'collapsed' : 'expanded'}`}>
{/* 折叠状态:图标栏 */}
<div className="sidebar-icons">
<button
className="icon-btn menu-btn"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
title="展开会话列表"
>
<Menu size={20} />
</button>
<button
className="icon-btn new-btn"
onClick={handleNewConversation}
title="新建对话"
>
<Plus size={20} />
</button>
{/* 最近会话图标 */}
<div className="recent-icons">
{conversations.slice(0, 5).map((conv) => (
<button
key={conv.id}
className={`icon-btn conv-icon ${conv.id === currentConversation?.id ? 'active' : ''}`}
onClick={() => handleSelectConversation(conv.id)}
title={conv.title}
>
<MessageSquare size={18} />
</button>
))}
</div>
<div className="sidebar-bottom">
<button className="icon-btn" title="设置">
<Settings size={20} />
</button>
</div>
</div>
{/* 展开状态:完整列表 */}
<div className="sidebar-content">
<div className="sidebar-header">
<button
className="icon-btn"
onClick={() => setSidebarCollapsed(true)}
>
<ChevronLeft size={20} />
</button>
<span className="sidebar-title"></span>
</div>
<button className="new-chat-btn" onClick={handleNewConversation}>
<Plus size={18} />
<span></span>
</button>
<div className="conversations-list">
{conversations.map((conv) => (
<div
key={conv.id}
className={`conv-item ${conv.id === currentConversation?.id ? 'active' : ''}`}
onClick={() => handleSelectConversation(conv.id)}
>
<MessageSquare size={16} />
<span className="conv-title">{conv.title}</span>
<button
className="conv-delete"
onClick={(e) => {
e.stopPropagation();
deleteConversation(conv.id);
}}
>
<Trash2 size={14} />
</button>
</div>
))}
</div>
</div>
</aside>
{/* 主工作区 */}
<main className="workspace">
{/* 顶部导航 */}
<header className="workspace-header">
<div className="header-left">
<button className="back-btn" onClick={handleBack}>
<ChevronLeft size={20} />
</button>
<div className="agent-info">
<div className="agent-icon">
<Bot size={20} />
</div>
<div className="agent-meta">
<h1 className="agent-name"> Agent</h1>
<div className="agent-status">
<span className="status-dot" />
<span className="status-text">Orchestrator Online</span>
</div>
</div>
</div>
</div>
<div className="header-right">
<div className="current-stage-badge">
<span className="badge-label">:</span>
<span className="badge-value">Step {currentStageIndex + 1}: {currentStageName}</span>
</div>
<button className="icon-btn">
<Settings size={20} />
</button>
</div>
</header>
{/* 聊天 + 状态面板 */}
<div className="workspace-body">
{/* 聊天区域 */}
<ChatArea
conversationId={currentConversation?.id}
context={context}
onContextUpdate={refreshContext}
/>
{/* 状态面板 */}
<StatePanel context={context} />
</div>
</main>
</div>
);
};
export default ProtocolAgentPage;