feat(aia): Protocol Agent streaming + editable state panel + protocol generation plan

Day 2 Development (2026-01-24):

Backend Enhancements:
- Implement SSE streaming in ProtocolAgentController using createStreamingService
- Add data condensation via LLM in ProtocolOrchestrator.handleProtocolSync
- Support stage editing without resetting progress
- Add explicit JSON output format for each stage in system prompt
- Create independent seed script for Protocol Agent (seed-protocol-agent.ts)

Frontend Improvements:
- Integrate useAIStream hook for typewriter effect in ChatArea
- Add MarkdownContent component for basic Markdown rendering
- Implement StageEditModal for editing stage data (scientific question, PICO, etc.)
- Add edit button to StageCard (visible on hover)
- Fix routing paths from /aia to /ai-qa
- Enhance CSS with full-screen layout and Markdown styles

New Documentation:
- One-click protocol generation development plan (v1.1)
- Editor selection evaluation (Novel vs BlockNote vs Tiptap)
- Novel fork strategy for AI-native editing

Technical Decisions:
- Choose Novel (Fork) as protocol editor for AI-first design
- Two-stage progressive generation: summary in chat, full protocol in editor
- 10-day development plan for protocol generation feature

Code Stats:
- Backend: 3 files modified, 1 new file
- Frontend: 9 files modified, 2 new files
- Docs: 3 new files

Status: Streaming and editable features working, protocol generation pending
This commit is contained in:
2026-01-24 23:06:33 +08:00
parent 596f2dfc02
commit 4d7d97ca19
18 changed files with 2708 additions and 192 deletions

View File

@@ -5,7 +5,7 @@
* 三栏布局:可折叠侧边栏 + 聊天区 + 状态面板
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import {
Bot, Settings, ChevronLeft, Menu, Plus,
@@ -15,6 +15,7 @@ import { ChatArea } from './components/ChatArea';
import { StatePanel } from './components/StatePanel';
import { useProtocolContext } from './hooks/useProtocolContext';
import { useProtocolConversations } from './hooks/useProtocolConversations';
import { getAccessToken } from '../../../framework/auth/api';
import './styles/protocol-agent.css';
export const ProtocolAgentPage: React.FC = () => {
@@ -36,27 +37,53 @@ export const ProtocolAgentPage: React.FC = () => {
// 上下文状态
const { context, refreshContext } = useProtocolContext(currentConversation?.id);
// 首次进入且无conversationId时自动创建新对话
const [isCreating, setIsCreating] = useState(false);
// 处理阶段数据编辑更新
const handleStageUpdate = useCallback(async (stageCode: string, data: Record<string, unknown>) => {
if (!currentConversation?.id) return;
const token = getAccessToken();
const response = await fetch('/api/v1/aia/protocol-agent/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
conversationId: currentConversation.id,
stageCode,
data,
}),
});
if (!response.ok) {
throw new Error('更新失败');
}
// 刷新上下文
await refreshContext();
}, [currentConversation?.id, refreshContext]);
// 使用ref避免无限循环
const hasTriedCreate = useRef(false);
useEffect(() => {
if (!conversationId && !currentConversation && !isCreating) {
// 只在首次进入且无conversationId时尝试创建一次
if (!conversationId && !currentConversation && !hasTriedCreate.current) {
hasTriedCreate.current = true;
console.log('[ProtocolAgentPage] 自动创建新对话...');
setIsCreating(true);
createConversation().then(newConv => {
if (newConv) {
console.log('[ProtocolAgentPage] 新对话创建成功:', newConv.id);
navigate(`/aia/protocol-agent/${newConv.id}`, { replace: true });
navigate(`/ai-qa/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]);
}, [conversationId, currentConversation, navigate]); // 移除createConversation依赖
// 获取当前阶段信息
const currentStageName = context?.stageName || '科学问题梳理';
@@ -66,7 +93,7 @@ export const ProtocolAgentPage: React.FC = () => {
const handleNewConversation = async () => {
const newConv = await createConversation();
if (newConv) {
navigate(`/aia/protocol-agent/${newConv.id}`);
navigate(`/ai-qa/protocol-agent/${newConv.id}`);
setSidebarCollapsed(true);
}
};
@@ -74,17 +101,17 @@ export const ProtocolAgentPage: React.FC = () => {
// 选择对话
const handleSelectConversation = (id: string) => {
selectConversation(id);
navigate(`/aia/protocol-agent/${id}`);
navigate(`/ai-qa/protocol-agent/${id}`);
setSidebarCollapsed(true);
};
// 返回AgentHub
const handleBack = () => {
navigate('/aia');
navigate('/ai-qa');
};
// 加载状态
if (isCreating || (!conversationId && !currentConversation)) {
// 如果没有conversationId显示等待状态
if (!conversationId) {
return (
<div className="protocol-agent-page">
<div style={{
@@ -103,7 +130,7 @@ export const ProtocolAgentPage: React.FC = () => {
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<p style={{ color: '#6B7280', fontSize: '14px' }}>...</p>
<p style={{ color: '#6B7280', fontSize: '14px' }}>...</p>
</div>
</div>
);
@@ -234,7 +261,7 @@ export const ProtocolAgentPage: React.FC = () => {
/>
{/* 状态面板 */}
<StatePanel context={context} />
<StatePanel context={context} onStageUpdate={handleStageUpdate} />
</div>
</main>
</div>