360 lines
11 KiB
TypeScript
360 lines
11 KiB
TypeScript
/**
|
||
* Protocol Agent 页面
|
||
*
|
||
* V2: 支持动态布局 + 文档预览
|
||
* - 要素收集阶段: Chat 65% : Context 35%
|
||
* - 方案生成阶段: Chat 35% : Document 65%
|
||
*/
|
||
|
||
import React, { useState, useEffect, useRef, useCallback } 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 { DocumentPanel } from './components/DocumentPanel';
|
||
import { ResizableSplitPane } from '@/shared/components/Layout';
|
||
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 }>();
|
||
|
||
// 侧边栏折叠状态
|
||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
||
|
||
// 会话管理
|
||
const {
|
||
conversations,
|
||
currentConversation,
|
||
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;
|
||
|
||
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]);
|
||
|
||
// 自动创建会话(首次进入时)
|
||
const hasAutoCreated = useRef(false);
|
||
|
||
useEffect(() => {
|
||
// 如果已有 conversationId 或 currentConversation,无需创建
|
||
if (conversationId || currentConversation || hasAutoCreated.current) {
|
||
return;
|
||
}
|
||
|
||
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) {
|
||
navigate(`/ai-qa/protocol-agent/${newConv.id}`);
|
||
setSidebarCollapsed(true);
|
||
}
|
||
};
|
||
|
||
// 选择对话
|
||
const handleSelectConversation = (id: string) => {
|
||
selectConversation(id);
|
||
navigate(`/ai-qa/protocol-agent/${id}`);
|
||
setSidebarCollapsed(true);
|
||
};
|
||
|
||
// 返回AgentHub
|
||
const handleBack = () => {
|
||
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 (
|
||
<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="back-to-hub-btn"
|
||
onClick={handleBack}
|
||
>
|
||
<ChevronLeft size={18} />
|
||
<span>返回</span>
|
||
</button>
|
||
</div>
|
||
|
||
<button className="new-chat-btn compact" onClick={handleNewConversation}>
|
||
<Plus size={14} />
|
||
<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">
|
||
{/* 视图切换器 */}
|
||
<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>
|
||
</div>
|
||
<button className="icon-btn">
|
||
<Settings size={20} />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
{/* 聊天 + 右侧面板(动态切换) */}
|
||
<div className="workspace-body">
|
||
<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}
|
||
/>
|
||
)
|
||
}
|
||
/>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ProtocolAgentPage;
|
||
|