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:
245
frontend-v2/src/modules/aia/protocol-agent/ProtocolAgentPage.tsx
Normal file
245
frontend-v2/src/modules/aia/protocol-agent/ProtocolAgentPage.tsx
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user