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:
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Protocol Agent 页面
|
||||
*
|
||||
* 100%还原原型图0119的精致设计
|
||||
* 三栏布局:可折叠侧边栏 + 聊天区 + 状态面板
|
||||
* V2: 支持动态布局 + 文档预览
|
||||
* - 要素收集阶段: Chat 65% : Context 35%
|
||||
* - 方案生成阶段: Chat 35% : Document 65%
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
@@ -13,11 +14,21 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { ChatArea } from './components/ChatArea';
|
||||
import { StatePanel } from './components/StatePanel';
|
||||
import { DocumentPanel } from './components/DocumentPanel';
|
||||
import { ResizableSplitPane } from './components/ResizableSplitPane';
|
||||
import { ViewSwitcher } from './components/ViewSwitcher';
|
||||
import { useProtocolContext } from './hooks/useProtocolContext';
|
||||
import { useProtocolConversations } from './hooks/useProtocolConversations';
|
||||
import { useProtocolGeneration } from './hooks/useProtocolGeneration';
|
||||
import { getAccessToken } from '../../../framework/auth/api';
|
||||
import './styles/protocol-agent.css';
|
||||
|
||||
// 布局比例配置
|
||||
const LAYOUT_RATIOS = {
|
||||
context: 65, // 要素收集阶段:Chat 65%
|
||||
document: 35, // 方案生成阶段:Chat 35%
|
||||
};
|
||||
|
||||
export const ProtocolAgentPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { conversationId } = useParams<{ conversationId?: string }>();
|
||||
@@ -32,11 +43,27 @@ export const ProtocolAgentPage: React.FC = () => {
|
||||
createConversation,
|
||||
selectConversation,
|
||||
deleteConversation,
|
||||
updateConversationTitle,
|
||||
} = useProtocolConversations(conversationId);
|
||||
|
||||
// 上下文状态
|
||||
const { context, refreshContext } = useProtocolContext(currentConversation?.id);
|
||||
|
||||
// 方案生成状态
|
||||
const {
|
||||
generatedContent,
|
||||
currentSection,
|
||||
isGenerating,
|
||||
hasGeneratedContent,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
startGeneration,
|
||||
exportWord,
|
||||
} = useProtocolGeneration({ conversationId: currentConversation?.id });
|
||||
|
||||
// Word 导出状态
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
// 处理阶段数据编辑更新
|
||||
const handleStageUpdate = useCallback(async (stageCode: string, data: Record<string, unknown>) => {
|
||||
if (!currentConversation?.id) return;
|
||||
@@ -63,33 +90,31 @@ export const ProtocolAgentPage: React.FC = () => {
|
||||
await refreshContext();
|
||||
}, [currentConversation?.id, refreshContext]);
|
||||
|
||||
// 使用ref避免无限循环
|
||||
const hasTriedCreate = useRef(false);
|
||||
// 自动创建会话(首次进入时)
|
||||
const hasAutoCreated = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 只在首次进入且无conversationId时尝试创建一次
|
||||
if (!conversationId && !currentConversation && !hasTriedCreate.current) {
|
||||
hasTriedCreate.current = true;
|
||||
console.log('[ProtocolAgentPage] 自动创建新对话...');
|
||||
|
||||
createConversation().then(newConv => {
|
||||
if (newConv) {
|
||||
console.log('[ProtocolAgentPage] 新对话创建成功:', newConv.id);
|
||||
navigate(`/ai-qa/protocol-agent/${newConv.id}`, { replace: true });
|
||||
} else {
|
||||
console.error('[ProtocolAgentPage] 创建对话失败');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('[ProtocolAgentPage] 创建对话异常:', err);
|
||||
});
|
||||
// 如果已有 conversationId 或 currentConversation,无需创建
|
||||
if (conversationId || currentConversation || hasAutoCreated.current) {
|
||||
return;
|
||||
}
|
||||
}, [conversationId, currentConversation, navigate]); // 移除createConversation依赖
|
||||
|
||||
hasAutoCreated.current = true;
|
||||
console.log('[ProtocolAgentPage] 自动创建新对话...');
|
||||
|
||||
createConversation().then(newConv => {
|
||||
if (newConv) {
|
||||
console.log('[ProtocolAgentPage] 新对话创建成功:', newConv.id);
|
||||
navigate(`/ai-qa/protocol-agent/${newConv.id}`, { replace: true });
|
||||
}
|
||||
});
|
||||
}, [conversationId, currentConversation, 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) {
|
||||
@@ -110,6 +135,35 @@ export const ProtocolAgentPage: React.FC = () => {
|
||||
navigate('/ai-qa');
|
||||
};
|
||||
|
||||
// 更新对话标题
|
||||
const handleTitleUpdate = (title: string) => {
|
||||
if (currentConversation?.id) {
|
||||
updateConversationTitle(currentConversation.id, title);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理 Word 导出
|
||||
const handleExportWord = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
await exportWord();
|
||||
} catch (error) {
|
||||
alert(`导出失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理章节讨论(Phase 2)
|
||||
const handleSectionDiscuss = (sectionId: string, sectionTitle: string) => {
|
||||
// TODO: 实现章节讨论模式
|
||||
console.log('讨论章节:', sectionId, sectionTitle);
|
||||
alert(`"讨论与优化" 功能将在 Phase 2 实现\n\n章节: ${sectionTitle}`);
|
||||
};
|
||||
|
||||
// 计算当前布局比例
|
||||
const currentLayoutRatio = LAYOUT_RATIOS[viewMode];
|
||||
|
||||
// 如果没有conversationId,显示等待状态
|
||||
if (!conversationId) {
|
||||
return (
|
||||
@@ -182,17 +236,17 @@ export const ProtocolAgentPage: React.FC = () => {
|
||||
<div className="sidebar-content">
|
||||
<div className="sidebar-header">
|
||||
<button
|
||||
className="icon-btn"
|
||||
onClick={() => setSidebarCollapsed(true)}
|
||||
className="back-to-hub-btn"
|
||||
onClick={handleBack}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
<ChevronLeft size={18} />
|
||||
<span>返回</span>
|
||||
</button>
|
||||
<span className="sidebar-title">研究方案</span>
|
||||
</div>
|
||||
|
||||
<button className="new-chat-btn" onClick={handleNewConversation}>
|
||||
<Plus size={18} />
|
||||
<span>新建方案</span>
|
||||
<button className="new-chat-btn compact" onClick={handleNewConversation}>
|
||||
<Plus size={14} />
|
||||
<span>新建对话</span>
|
||||
</button>
|
||||
|
||||
<div className="conversations-list">
|
||||
@@ -241,6 +295,13 @@ export const ProtocolAgentPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
{/* 视图切换器 */}
|
||||
<ViewSwitcher
|
||||
currentView={viewMode}
|
||||
onViewChange={setViewMode}
|
||||
documentDisabled={!hasGeneratedContent && !isGenerating}
|
||||
documentDisabledTooltip="请先生成研究方案"
|
||||
/>
|
||||
<div className="current-stage-badge">
|
||||
<span className="badge-label">当前阶段:</span>
|
||||
<span className="badge-value">Step {currentStageIndex + 1}: {currentStageName}</span>
|
||||
@@ -251,17 +312,43 @@ export const ProtocolAgentPage: React.FC = () => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 聊天 + 状态面板 */}
|
||||
{/* 聊天 + 右侧面板(动态切换) */}
|
||||
<div className="workspace-body">
|
||||
{/* 聊天区域 */}
|
||||
<ChatArea
|
||||
conversationId={currentConversation?.id}
|
||||
context={context}
|
||||
onContextUpdate={refreshContext}
|
||||
<ResizableSplitPane
|
||||
defaultLeftRatio={currentLayoutRatio}
|
||||
minLeftRatio={25}
|
||||
maxLeftRatio={80}
|
||||
enableDrag={true}
|
||||
storageKey={`protocol-agent-split-${viewMode}`}
|
||||
leftPanel={
|
||||
<ChatArea
|
||||
conversationId={currentConversation?.id}
|
||||
context={context}
|
||||
onContextUpdate={refreshContext}
|
||||
onTitleUpdate={handleTitleUpdate}
|
||||
/>
|
||||
}
|
||||
rightPanel={
|
||||
viewMode === 'context' ? (
|
||||
<StatePanel
|
||||
context={context}
|
||||
onStageUpdate={handleStageUpdate}
|
||||
onStartGeneration={startGeneration}
|
||||
canGenerate={context?.canGenerate}
|
||||
isGenerating={isGenerating}
|
||||
/>
|
||||
) : (
|
||||
<DocumentPanel
|
||||
content={generatedContent}
|
||||
isGenerating={isGenerating}
|
||||
currentSection={currentSection || undefined}
|
||||
onExportWord={handleExportWord}
|
||||
isExporting={isExporting}
|
||||
onSectionDiscuss={handleSectionDiscuss}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 状态面板 */}
|
||||
<StatePanel context={context} onStageUpdate={handleStageUpdate} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user