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

@@ -107,6 +107,8 @@ vite.config.*.timestamp-*

View File

@@ -74,6 +74,8 @@ exec nginx -g 'daemon off;'

View File

@@ -230,6 +230,8 @@ http {

View File

@@ -59,4 +59,6 @@ export default apiClient;

View File

@@ -257,5 +257,7 @@ export async function logout(): Promise<void> {

View File

@@ -23,5 +23,7 @@ export * from './api';

View File

@@ -49,3 +49,5 @@ export async function fetchUserModules(): Promise<string[]> {

View File

@@ -129,3 +129,5 @@ export default ModulePermissionModal;

View File

@@ -40,3 +40,5 @@ export default AdminModule;

View File

@@ -205,3 +205,5 @@ export const TENANT_TYPE_NAMES: Record<TenantType, string> = {

View File

@@ -36,7 +36,7 @@ const getIcon = (iconName: string): React.ComponentType<any> => {
export const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick }) => {
const Icon = getIcon(agent.icon);
const orderStr = String(agent.order).padStart(2, '0');
const orderStr = agent.isProtocolAgent ? '🚀' : String(agent.order).padStart(2, '0');
const handleClick = () => {
if (agent.isTool && agent.toolUrl) {
@@ -47,9 +47,16 @@ export const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick }) => {
}
};
const cardClasses = [
'agent-card',
`theme-${agent.theme}`,
agent.isTool ? 'tool-card' : '',
agent.isProtocolAgent ? 'protocol-agent-card' : '',
].filter(Boolean).join(' ');
return (
<div
className={`agent-card theme-${agent.theme} ${agent.isTool ? 'tool-card' : ''}`}
className={cardClasses}
onClick={handleClick}
>
{/* 工具类:右上角跳转图标 */}
@@ -89,3 +96,4 @@ export default AgentCard;

View File

@@ -86,6 +86,39 @@ export const AgentHub: React.FC<AgentHubProps> = ({ onAgentSelect }) => {
const isLast = phaseIndex === PHASES.length - 1;
const agents = agentsByPhase[phase.phase] || [];
// Protocol Agent 阶段特殊处理phase 0单独显示1个卡片
if (phase.isProtocolAgent) {
return (
<div key={phase.phase} className="pipeline-stage protocol-stage">
{/* 左侧时间轴 - 星号 */}
<div className="timeline-marker">
<div className="timeline-dot theme-indigo protocol-dot">
</div>
<div className="timeline-line" />
</div>
{/* 阶段内容 */}
<div className="stage-content">
<h2 className="stage-title protocol-title">
{phase.name}
<span className="beta-badge"></span>
</h2>
<div className="agents-grid grid-cols-1 protocol-grid">
{agents.map(agent => (
<AgentCard
key={agent.id}
agent={agent}
onClick={onAgentSelect}
/>
))}
</div>
</div>
</div>
);
}
// 根据阶段确定列数
let gridCols = 'grid-cols-3';
if (phase.phase === 2) gridCols = 'grid-cols-3'; // 4个卡片每行3个

View File

@@ -19,3 +19,5 @@ export { ChatWorkspace } from './ChatWorkspace';

View File

@@ -10,6 +10,7 @@ import type { AgentConfig, PhaseConfig } from './types';
* 阶段配置
*/
export const PHASES: PhaseConfig[] = [
{ phase: 0, name: '全流程研究方案制定', theme: 'indigo', isProtocolAgent: true },
{ phase: 1, name: '选题优化智能体', theme: 'blue' },
{ phase: 2, name: '方案设计智能体', theme: 'blue' },
{ phase: 3, name: '方案预评审', theme: 'yellow' },
@@ -17,10 +18,27 @@ export const PHASES: PhaseConfig[] = [
{ phase: 5, name: '写作助手', theme: 'purple' },
];
/**
* Protocol Agent 配置(全流程研究方案制定)
*/
export const PROTOCOL_AGENT: AgentConfig = {
id: 'PROTOCOL_AGENT',
name: '全流程研究方案制定',
icon: 'workflow',
description: '一站式完成研究方案核心要素科学问题→PICO→研究设计→样本量→观察指标支持一键生成完整方案。',
theme: 'indigo',
phase: 0,
order: 0,
isProtocolAgent: true,
};
/**
* 12个智能体配置
*/
export const AGENTS: AgentConfig[] = [
// Phase 0: Protocol Agent全流程
PROTOCOL_AGENT,
// Phase 1: 选题优化智能体 (3个每行3个)
{
id: 'TOPIC_01',
@@ -183,3 +201,4 @@ export const BRAND_COLORS = {

View File

@@ -1,49 +1,80 @@
/**
* AIA - AI Intelligent Assistant 模块入口
*
* 视图管理:
* - Hub: 智能体大厅12个模块展示
* - Chat: 沉浸式对话工作台
* 路由管理:
* - /aia -> Hub: 智能体大厅12个模块展示
* - /aia/chat -> Chat: 沉浸式对话工作台原12个智能体
* - /aia/protocol-agent/:conversationId? -> Protocol Agent全流程方案制定
*/
import React, { useState } from 'react';
import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
import { AgentHub } from './components/AgentHub';
import { ChatWorkspace } from './components/ChatWorkspace';
import { ProtocolAgentPage } from './protocol-agent';
import type { AgentConfig } from './types';
const AIAModule: React.FC = () => {
const [currentView, setCurrentView] = useState<'hub' | 'chat'>('hub');
const [selectedAgent, setSelectedAgent] = useState<AgentConfig | null>(null);
const [initialQuery, setInitialQuery] = useState<string | undefined>();
return (
<Routes>
{/* 智能体大厅 */}
<Route index element={<AIAHub />} />
{/* Protocol Agent全流程研究方案制定 */}
<Route path="protocol-agent/:conversationId?" element={<ProtocolAgentPage />} />
{/* 传统智能体对话(向后兼容) */}
<Route path="chat" element={<AIAChat />} />
</Routes>
);
};
/**
* Hub 页面
*/
const AIAHub: React.FC = () => {
const navigate = useNavigate();
// 选择智能体,进入对话
const handleAgentSelect = (agent: AgentConfig & { initialQuery?: string }) => {
setSelectedAgent(agent);
setInitialQuery(agent.initialQuery);
setCurrentView('chat');
console.log('[AIAHub] Agent selected:', agent.id, 'isProtocolAgent:', agent.isProtocolAgent);
if (agent.isProtocolAgent) {
// Protocol Agent跳转专属页面
console.log('[AIAHub] Navigating to /aia/protocol-agent');
navigate('/aia/protocol-agent');
} else {
// 传统智能体:跳转对话页面
console.log('[AIAHub] Navigating to /aia/chat');
navigate('/aia/chat', { state: { agent, initialQuery: agent.initialQuery } });
}
};
// 返回大厅
return <AgentHub onAgentSelect={handleAgentSelect} />;
};
/**
* 传统智能体对话页面
*/
const AIAChat: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const state = location.state as { agent: AgentConfig; initialQuery?: string } | null;
const handleBack = () => {
setCurrentView('hub');
setSelectedAgent(null);
setInitialQuery(undefined);
navigate('/aia');
};
if (!state?.agent) {
navigate('/aia');
return null;
}
return (
<>
{currentView === 'hub' && (
<AgentHub onAgentSelect={handleAgentSelect} />
)}
{currentView === 'chat' && selectedAgent && (
<ChatWorkspace
agent={selectedAgent}
initialQuery={initialQuery}
onBack={handleBack}
/>
)}
</>
<ChatWorkspace
agent={state.agent}
initialQuery={state.initialQuery}
onBack={handleBack}
/>
);
};

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;

View File

@@ -0,0 +1,66 @@
/**
* Action Card - Deep Link 动作卡片
*
* 100%还原原型图Action Card设计
*/
import React from 'react';
import { Calculator, ExternalLink, Rocket } from 'lucide-react';
import type { ActionCard } from '../types';
interface ActionCardProps {
card: ActionCard;
}
const CARD_ICONS: Record<string, React.ComponentType<any>> = {
sample_size_calculator: Calculator,
generate_protocol: Rocket,
};
export const ActionCardComponent: React.FC<ActionCardProps> = ({ card }) => {
const Icon = CARD_ICONS[card.id] || Calculator;
const handleClick = () => {
if (card.type === 'tool') {
// 打开工具页面(新标签页或模态框)
window.open(card.actionUrl, '_blank');
} else if (card.type === 'action') {
// 触发动作(如一键生成)
// 这里会被父组件处理
console.log('Action triggered:', card.id);
}
};
return (
<div className={`action-card ${card.type}`} onClick={handleClick}>
<div className="action-card-header">
<div className="action-icon-box">
<Icon size={20} />
</div>
<div className="action-info">
<h3 className="action-title">{card.title}</h3>
<p className="action-description">{card.description}</p>
</div>
</div>
{card.type === 'tool' && (
<div className="action-footer">
<button className="action-button">
<span>🚀 </span>
<ExternalLink size={14} />
</button>
<p className="action-hint"></p>
</div>
)}
{card.type === 'action' && (
<button className="action-button primary">
<span></span>
<Rocket size={14} />
</button>
)}
</div>
);
};

View File

@@ -0,0 +1,310 @@
/**
* Chat Area - Protocol Agent 聊天区域
*
* 基于通用Chat组件扩展Protocol Agent特有功能
*/
import React, { useState, useRef, useEffect } from 'react';
import { Send, Sparkles, User } from 'lucide-react';
import type { ProtocolContext, AgentResponse } from '../types';
import { SyncButton } from './SyncButton';
import { ActionCardComponent } from './ActionCard';
import { ReflexionMessage } from './ReflexionMessage';
import { getAccessToken } from '../../../../framework/auth/api';
interface ChatAreaProps {
conversationId?: string;
context: ProtocolContext | null;
onContextUpdate: () => void;
}
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
thinkingContent?: string;
syncButton?: AgentResponse['syncButton'];
actionCards?: AgentResponse['actionCards'];
timestamp: Date;
}
const API_BASE = '/api/v1/aia/protocol-agent';
export const ChatArea: React.FC<ChatAreaProps> = ({
conversationId,
context,
onContextUpdate
}) => {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const chatContainerRef = useRef<HTMLDivElement>(null);
// 自动滚动到底部
const scrollToBottom = () => {
if (chatContainerRef.current) {
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// 初始化欢迎消息
useEffect(() => {
if (conversationId && messages.length === 0) {
setMessages([{
id: 'welcome',
role: 'assistant',
content: `您好!我是研究方案制定助手,将帮助您系统地完成临床研究方案的核心要素设计。
我们将一起完成以下5个关键步骤
1⃣ **科学问题梳理** - 明确研究要解决的核心问题
2⃣ **PICO要素** - 确定研究人群、干预、对照和结局
3⃣ **研究设计** - 选择合适的研究类型和方法
4⃣ **样本量计算** - 估算所需的样本量
5⃣ **观察指标** - 定义基线、暴露、结局指标和混杂因素
完成这5个要素后您可以**一键生成完整的研究方案**并下载为Word文档。
让我们开始吧!请先告诉我,您想研究什么问题?或者描述一下您的研究背景和想法。`,
timestamp: new Date(),
}]);
}
}, [conversationId, messages.length]);
/**
* 发送消息
*/
const handleSend = async () => {
if (!input.trim() || !conversationId) return;
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: input.trim(),
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setLoading(true);
try {
const token = getAccessToken();
const response = await fetch(`${API_BASE}/message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
conversationId,
content: userMessage.content,
}),
});
if (!response.ok) {
throw new Error('Failed to send message');
}
const result = await response.json();
if (result.success && result.data) {
const aiResponse: AgentResponse = result.data;
const aiMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: aiResponse.content,
thinkingContent: aiResponse.thinkingContent,
syncButton: aiResponse.syncButton,
actionCards: aiResponse.actionCards,
timestamp: new Date(),
};
setMessages(prev => [...prev, aiMessage]);
// 刷新上下文状态
onContextUpdate();
}
} catch (err) {
console.error('[ChatArea] handleSend error:', err);
// 显示错误消息
setMessages(prev => [...prev, {
id: (Date.now() + 1).toString(),
role: 'system',
content: '抱歉,消息发送失败。请稍后重试。',
timestamp: new Date(),
}]);
} finally {
setLoading(false);
}
};
/**
* 处理同步
*/
const handleSync = async (stageCode: string, data: Record<string, unknown>) => {
if (!conversationId) return;
try {
const token = getAccessToken();
const response = await fetch(`${API_BASE}/sync`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
conversationId,
stageCode,
data,
}),
});
if (!response.ok) {
throw new Error('Failed to sync data');
}
const result = await response.json();
if (result.success) {
// 添加系统消息
const systemMsg: Message = {
id: Date.now().toString(),
role: 'system',
content: result.data.message || '✅ 已同步到方案',
timestamp: new Date(),
};
setMessages(prev => [...prev, systemMsg]);
// 刷新上下文
onContextUpdate();
}
} catch (err) {
console.error('[ChatArea] handleSync error:', err);
}
};
// 按Enter发送
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<section className="chat-area">
{/* 聊天历史 */}
<div className="chat-container" ref={chatContainerRef}>
{messages.map(msg => (
<div key={msg.id}>
{msg.role === 'user' && (
<div className="message-row user-row">
<div className="avatar user-avatar">
<User size={16} />
</div>
<div className="message-content">
<div className="message-meta">User {formatTime(msg.timestamp)}</div>
<div className="chat-bubble user-bubble">
{msg.content}
</div>
</div>
</div>
)}
{msg.role === 'assistant' && (
<div className="message-row assistant-row">
<div className="avatar assistant-avatar">
<Sparkles size={16} />
</div>
<div className="message-content">
<div className="message-meta">Protocol Agent {formatTime(msg.timestamp)}</div>
<div className="chat-bubble assistant-bubble">
{msg.content}
</div>
{/* 同步按钮 */}
{msg.syncButton && (
<SyncButton
syncData={msg.syncButton}
onSync={handleSync}
/>
)}
{/* Action Cards */}
{msg.actionCards && msg.actionCards.length > 0 && (
<div className="action-cards">
{msg.actionCards.map(card => (
<ActionCardComponent key={card.id} card={card} />
))}
</div>
)}
</div>
</div>
)}
{msg.role === 'system' && (
<div className="message-row system-row">
<div className="system-message">
{msg.content}
</div>
</div>
)}
</div>
))}
{loading && (
<div className="message-row assistant-row">
<div className="avatar assistant-avatar">
<Sparkles size={16} />
</div>
<div className="message-content">
<div className="chat-bubble assistant-bubble thinking">
<span className="thinking-dot"></span>
<span className="thinking-dot"></span>
<span className="thinking-dot"></span>
</div>
</div>
</div>
)}
</div>
{/* 输入区 */}
<div className="input-area">
<div className="input-wrapper">
<input
type="text"
className="input-field"
placeholder="输入您的指令..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={loading || !conversationId}
/>
<button
className="send-btn"
onClick={handleSend}
disabled={loading || !input.trim() || !conversationId}
>
<Send size={16} />
</button>
</div>
</div>
</section>
);
};
/**
* 格式化时间
*/
function formatTime(date: Date): string {
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}

View File

@@ -0,0 +1,52 @@
/**
* Reflexion Message - 反思校验消息
*
* 100%还原原型图Reflexion设计紫色左边框
*/
import React from 'react';
import { ShieldCheck, CheckCheck } from 'lucide-react';
interface ReflexionMessageProps {
content: string;
timestamp?: Date;
}
export const ReflexionMessage: React.FC<ReflexionMessageProps> = ({
content,
timestamp = new Date()
}) => {
return (
<div className="message-row reflexion-row">
<div className="avatar reflexion-avatar">
<CheckCheck size={16} />
</div>
<div className="message-content">
<div className="message-meta reflexion-meta">
Reflexion Guard {formatTime(timestamp)}
</div>
<div className="chat-bubble reflexion-bubble">
<div className="reflexion-header">
<ShieldCheck size={14} />
<span></span>
</div>
<div className="reflexion-content">
{content}
</div>
</div>
</div>
</div>
);
};
/**
* 格式化时间
*/
function formatTime(date: Date): string {
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}

View File

@@ -0,0 +1,237 @@
/**
* Stage Card - 阶段状态卡片
*
* 100%还原原型图阶段卡片设计
*/
import React from 'react';
import { Check, Loader2, Edit2 } from 'lucide-react';
import type {
StageInfo,
ScientificQuestionData,
PICOData,
StudyDesignData,
SampleSizeData,
EndpointsData
} from '../types';
interface StageCardProps {
stage: StageInfo;
index: number;
}
const STAGE_TITLES: Record<string, string> = {
scientific_question: '科学问题',
pico: 'PICO',
study_design: '研究设计',
sample_size: '样本量',
endpoints: '观察指标',
};
export const StageCard: React.FC<StageCardProps> = ({ stage, index }) => {
const { stageCode, status, data } = stage;
const title = STAGE_TITLES[stageCode] || stage.stageName;
const number = (index + 1).toString().padStart(2, '0');
// 根据状态确定样式
const cardClasses = [
'stage-card',
status === 'completed' ? 'completed' : '',
status === 'current' ? 'current' : '',
status === 'pending' ? 'pending' : '',
].filter(Boolean).join(' ');
return (
<div className={cardClasses}>
<div className="stage-header">
<h3 className="stage-number">{number} {title}</h3>
{status === 'completed' && <Check size={14} className="check-icon" />}
{status === 'current' && <Loader2 size={14} className="loader-icon animate-spin" />}
</div>
{data && <StageDataRenderer stageCode={stageCode} data={data} />}
</div>
);
};
/**
* 渲染不同阶段的数据
*/
const StageDataRenderer: React.FC<{
stageCode: string;
data: ScientificQuestionData | PICOData | StudyDesignData | SampleSizeData | EndpointsData;
}> = ({ stageCode, data }) => {
switch (stageCode) {
case 'scientific_question':
return <ScientificQuestionCard data={data as ScientificQuestionData} />;
case 'pico':
return <PICOCard data={data as PICOData} />;
case 'study_design':
return <StudyDesignCard data={data as StudyDesignData} />;
case 'sample_size':
return <SampleSizeCard data={data as SampleSizeData} />;
case 'endpoints':
return <EndpointsCard data={data as EndpointsData} />;
default:
return null;
}
};
/**
* 科学问题卡片
*/
const ScientificQuestionCard: React.FC<{ data: ScientificQuestionData }> = ({ data }) => (
<div className="stage-data">
<p className="data-content">{data.content}</p>
</div>
);
/**
* PICO卡片
*/
const PICOCard: React.FC<{ data: PICOData }> = ({ data }) => (
<div className="stage-data pico-data">
<div className="pico-item">
<span className="pico-label p">P</span>
<span className="pico-value">{data.population}</span>
</div>
<div className="pico-item">
<span className="pico-label i">I</span>
<span className="pico-value">{data.intervention}</span>
</div>
<div className="pico-item">
<span className="pico-label c">C</span>
<span className="pico-value">{data.comparison}</span>
</div>
<div className="pico-item">
<span className="pico-label o">O</span>
<span className="pico-value">{data.outcome}</span>
</div>
</div>
);
/**
* 研究设计卡片
*/
const StudyDesignCard: React.FC<{ data: StudyDesignData }> = ({ data }) => (
<div className="stage-data">
<div className="design-tags">
<span className="design-tag">{data.studyType}</span>
{data.design?.map((item, i) => (
<span key={i} className="design-tag">{item}</span>
))}
</div>
</div>
);
/**
* 样本量卡片
*/
const SampleSizeCard: React.FC<{ data: SampleSizeData }> = ({ data }) => (
<div className="stage-data sample-size-data">
<div className="sample-size-row">
<span className="label"> (N)</span>
<span className="value">N = {data.sampleSize}</span>
</div>
{data.calculation && (
<div className="calculation-params">
{data.calculation.alpha && (
<div className="param-item">
<span>α = {data.calculation.alpha}</span>
</div>
)}
{data.calculation.power && (
<div className="param-item">
<span>Power = {data.calculation.power}</span>
</div>
)}
</div>
)}
</div>
);
/**
* 观察指标卡片
*/
const EndpointsCard: React.FC<{ data: EndpointsData }> = ({ data }) => (
<div className="stage-data endpoints-data">
{/* 基线指标 */}
{data.baseline && Object.values(data.baseline).some(arr => arr && arr.length > 0) && (
<div className="endpoint-section">
<div className="endpoint-title">📊 线</div>
<div className="endpoint-tags">
{data.baseline.demographics?.map((item, i) => (
<span key={i} className="endpoint-tag">{item}</span>
))}
{data.baseline.clinicalHistory?.map((item, i) => (
<span key={i} className="endpoint-tag">{item}</span>
))}
</div>
</div>
)}
{/* 暴露指标 */}
{data.exposure && (
<div className="endpoint-section">
<div className="endpoint-title">💊 </div>
{data.exposure.intervention && (
<div className="endpoint-item">
<span className="item-label">:</span>
<span className="item-value">{data.exposure.intervention}</span>
</div>
)}
{data.exposure.control && (
<div className="endpoint-item">
<span className="item-label">:</span>
<span className="item-value">{data.exposure.control}</span>
</div>
)}
</div>
)}
{/* 结局指标 */}
{data.outcomes && (
<div className="endpoint-section">
<div className="endpoint-title">🎯 </div>
{data.outcomes.primary && data.outcomes.primary.length > 0 && (
<div className="endpoint-subsection">
<span className="subsection-label">:</span>
{data.outcomes.primary.map((item, i) => (
<span key={i} className="endpoint-tag primary">{item}</span>
))}
</div>
)}
{data.outcomes.secondary && data.outcomes.secondary.length > 0 && (
<div className="endpoint-subsection">
<span className="subsection-label">:</span>
{data.outcomes.secondary.map((item, i) => (
<span key={i} className="endpoint-tag">{item}</span>
))}
</div>
)}
{data.outcomes.safety && data.outcomes.safety.length > 0 && (
<div className="endpoint-subsection">
<span className="subsection-label">:</span>
{data.outcomes.safety.map((item, i) => (
<span key={i} className="endpoint-tag safety">{item}</span>
))}
</div>
)}
</div>
)}
{/* 混杂因素 */}
{data.confounders && data.confounders.length > 0 && (
<div className="endpoint-section">
<div className="endpoint-title"> </div>
<div className="endpoint-tags">
{data.confounders.map((item, i) => (
<span key={i} className="endpoint-tag confounder">{item}</span>
))}
</div>
</div>
)}
</div>
);

View File

@@ -0,0 +1,90 @@
/**
* State Panel - 方案状态面板
*
* 100%还原原型图右侧状态面板设计
*/
import React from 'react';
import { FileText, Check, Loader2 } from 'lucide-react';
import type { ProtocolContext, StageInfo } from '../types';
import { StageCard } from './StageCard';
interface StatePanelProps {
context: ProtocolContext | null;
}
export const StatePanel: React.FC<StatePanelProps> = ({ context }) => {
if (!context) {
return (
<aside className="state-panel">
<div className="panel-header">
<h2 className="panel-title">
<FileText size={16} />
(Context)
</h2>
<span className="status-badge waiting">Waiting</span>
</div>
<div className="panel-body">
<div className="empty-state">
<Loader2 size={32} className="animate-spin text-gray-300" />
<p className="text-sm text-gray-400 mt-4">...</p>
</div>
</div>
</aside>
);
}
const { stages, progress, canGenerate } = context;
return (
<aside className="state-panel">
{/* 头部 */}
<div className="panel-header">
<h2 className="panel-title">
<FileText size={16} />
(Context)
</h2>
<span className={`status-badge ${canGenerate ? 'completed' : 'synced'}`}>
{canGenerate ? 'Completed' : 'Synced'}
</span>
</div>
{/* 进度条 */}
<div className="progress-bar-container">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${progress}%` }}
/>
</div>
<span className="progress-text">{progress}%</span>
</div>
{/* 阶段列表 */}
<div className="panel-body">
{stages.map((stage, index) => (
<StageCard
key={stage.stageCode}
stage={stage}
index={index}
/>
))}
{/* 一键生成按钮 */}
{canGenerate && (
<div className="generate-section">
<button className="generate-btn">
<span className="generate-icon">🚀</span>
<span className="generate-text"></span>
</button>
<p className="generate-hint">
5
</p>
</div>
)}
</div>
</aside>
);
};

View File

@@ -0,0 +1,50 @@
/**
* Sync Button - 同步到方案按钮
*
* 100%还原原型图同步按钮设计
*/
import React, { useState } from 'react';
import { RefreshCw, Check } from 'lucide-react';
import type { SyncButtonData } from '../types';
interface SyncButtonProps {
syncData: SyncButtonData;
onSync: (stageCode: string, data: Record<string, unknown>) => Promise<void>;
}
export const SyncButton: React.FC<SyncButtonProps> = ({ syncData, onSync }) => {
const [syncing, setSyncing] = useState(false);
const handleClick = async () => {
if (syncData.disabled || syncing) return;
setSyncing(true);
try {
await onSync(syncData.stageCode, syncData.extractedData);
} finally {
setSyncing(false);
}
};
return (
<div className="sync-button-wrapper">
<button
className={`sync-button ${syncData.disabled ? 'disabled' : ''} ${syncing ? 'syncing' : ''}`}
onClick={handleClick}
disabled={syncData.disabled || syncing}
>
{syncing ? (
<RefreshCw size={14} className="sync-icon spinning" />
) : syncData.disabled ? (
<Check size={14} className="sync-icon" />
) : (
<span className="sync-icon"></span>
)}
<span className="sync-label">{syncData.label}</span>
</button>
</div>
);
};

View File

@@ -0,0 +1,12 @@
/**
* Protocol Agent Components Export
*/
export { ChatArea } from './ChatArea';
export { StatePanel } from './StatePanel';
export { StageCard } from './StageCard';
export { SyncButton } from './SyncButton';
export { ActionCardComponent } from './ActionCard';
export { ReflexionMessage } from './ReflexionMessage';

View File

@@ -0,0 +1,8 @@
/**
* Protocol Agent Hooks Export
*/
export { useProtocolContext } from './useProtocolContext';
export { useProtocolConversations } from './useProtocolConversations';

View File

@@ -0,0 +1,79 @@
/**
* useProtocolContext Hook
* 管理Protocol Agent的上下文状态
*/
import { useState, useEffect, useCallback } from 'react';
import type { ProtocolContext } from '../types';
import { getAccessToken } from '../../../../framework/auth/api';
const API_BASE = '/api/v1/aia/protocol-agent';
export function useProtocolContext(conversationId?: string) {
const [context, setContext] = useState<ProtocolContext | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/**
* 获取上下文状态
*/
const fetchContext = useCallback(async () => {
if (!conversationId) {
setContext(null);
return;
}
setLoading(true);
setError(null);
try {
const token = getAccessToken();
const response = await fetch(`${API_BASE}/context/${conversationId}`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.status === 404) {
// 上下文不存在,返回空
setContext(null);
return;
}
if (!response.ok) {
throw new Error(`Failed to fetch context: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
setContext(result.data);
}
} catch (err) {
console.error('[useProtocolContext] fetchContext error:', err);
setError(err instanceof Error ? err.message : 'Failed to load context');
} finally {
setLoading(false);
}
}, [conversationId]);
/**
* 刷新上下文
*/
const refreshContext = useCallback(() => {
fetchContext();
}, [fetchContext]);
// 初次加载和conversationId变化时获取上下文
useEffect(() => {
fetchContext();
}, [fetchContext]);
return {
context,
loading,
error,
refreshContext,
};
}

View File

@@ -0,0 +1,144 @@
/**
* useProtocolConversations Hook
* 管理Protocol Agent的会话列表
*/
import { useState, useEffect, useCallback } from 'react';
import type { ProtocolConversation } from '../types';
import { getAccessToken } from '../../../../framework/auth/api';
const API_BASE = '/api/v1/aia';
export function useProtocolConversations(initialConversationId?: string) {
const [conversations, setConversations] = useState<ProtocolConversation[]>([]);
const [currentConversation, setCurrentConversation] = useState<ProtocolConversation | null>(null);
const [loading, setLoading] = useState(false);
/**
* 获取会话列表
*/
const fetchConversations = useCallback(async () => {
setLoading(true);
try {
const token = getAccessToken();
const response = await fetch(`${API_BASE}/conversations`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch conversations');
}
const result = await response.json();
const allConversations = result.data || result;
// 过滤出Protocol Agent的对话agentId为PROTOCOL_AGENT
const protocolConversations = Array.isArray(allConversations)
? allConversations.filter((conv: any) =>
conv.agentId === 'PROTOCOL_AGENT' || conv.agent_id === 'PROTOCOL_AGENT'
)
: [];
setConversations(protocolConversations);
// 如果有initialConversationId设置为当前对话
if (initialConversationId) {
const current = protocolConversations.find((c: any) => c.id === initialConversationId);
if (current) {
setCurrentConversation(current);
}
}
} catch (err) {
console.error('[useProtocolConversations] fetchConversations error:', err);
} finally {
setLoading(false);
}
}, [initialConversationId]);
/**
* 创建新对话
*/
const createConversation = useCallback(async (): Promise<ProtocolConversation | null> => {
try {
const token = getAccessToken();
const response = await fetch(`${API_BASE}/conversations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
agentId: 'PROTOCOL_AGENT',
title: `研究方案-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`,
}),
});
if (!response.ok) {
throw new Error('Failed to create conversation');
}
const result = await response.json();
const newConv = result.data || result;
setConversations(prev => [newConv, ...prev]);
setCurrentConversation(newConv);
return newConv;
} catch (err) {
console.error('[useProtocolConversations] createConversation error:', err);
return null;
}
}, []);
/**
* 选择对话
*/
const selectConversation = useCallback((id: string) => {
const conv = conversations.find(c => c.id === id);
if (conv) {
setCurrentConversation(conv);
}
}, [conversations]);
/**
* 删除对话
*/
const deleteConversation = useCallback(async (id: string) => {
try {
const token = getAccessToken();
await fetch(`${API_BASE}/conversations/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
},
});
setConversations(prev => prev.filter(c => c.id !== id));
if (currentConversation?.id === id) {
setCurrentConversation(null);
}
} catch (err) {
console.error('[useProtocolConversations] deleteConversation error:', err);
}
}, [currentConversation]);
// 初次加载
useEffect(() => {
fetchConversations();
}, [fetchConversations]);
return {
conversations,
currentConversation,
loading,
createConversation,
selectConversation,
deleteConversation,
refreshConversations: fetchConversations,
};
}

View File

@@ -0,0 +1,10 @@
/**
* Protocol Agent Module Export
*/
export { default as ProtocolAgentPage } from './ProtocolAgentPage';
export * from './types';
export * from './components';
export * from './hooks';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,175 @@
/**
* Protocol Agent 类型定义
*/
export type ProtocolStageCode =
| 'scientific_question'
| 'pico'
| 'study_design'
| 'sample_size'
| 'endpoints';
export type StageStatus = 'completed' | 'current' | 'pending';
/**
* 科学问题数据
*/
export interface ScientificQuestionData {
content: string;
background?: string;
significance?: string;
confirmed?: boolean;
confirmedAt?: string;
}
/**
* PICO数据
*/
export interface PICOData {
population: string;
intervention: string;
comparison: string;
outcome: string;
confirmed?: boolean;
confirmedAt?: string;
}
/**
* 研究设计数据
*/
export interface StudyDesignData {
studyType: string;
design: string[];
features?: string[];
confirmed?: boolean;
confirmedAt?: string;
}
/**
* 样本量数据
*/
export interface SampleSizeData {
sampleSize: number;
calculation?: {
alpha?: number;
power?: number;
effectSize?: number;
dropoutRate?: number;
};
confirmed?: boolean;
confirmedAt?: string;
}
/**
* 观察指标数据
*/
export interface EndpointsData {
baseline?: {
demographics?: string[];
clinicalHistory?: string[];
laboratoryTests?: string[];
};
exposure?: {
intervention?: string;
control?: string;
dosage?: string;
duration?: string;
};
outcomes?: {
primary?: string[];
secondary?: string[];
safety?: string[];
};
confounders?: string[];
confirmed?: boolean;
confirmedAt?: string;
}
/**
* 阶段状态
*/
export interface StageInfo {
stageCode: ProtocolStageCode;
stageName: string;
status: StageStatus;
data: ScientificQuestionData | PICOData | StudyDesignData | SampleSizeData | EndpointsData | null;
}
/**
* Protocol上下文
*/
export interface ProtocolContext {
currentStage: ProtocolStageCode;
stageName: string;
progress: number;
stages: StageInfo[];
canGenerate: boolean;
}
/**
* 同步按钮数据
*/
export interface SyncButtonData {
stageCode: ProtocolStageCode;
extractedData: Record<string, unknown>;
label: string;
disabled: boolean;
}
/**
* Action Card数据
*/
export interface ActionCard {
id: string;
type: 'tool' | 'action';
title: string;
description: string;
actionUrl: string;
}
/**
* Agent响应
*/
export interface AgentResponse {
content: string;
thinkingContent?: string;
stage: ProtocolStageCode;
stageName: string;
syncButton?: SyncButtonData;
actionCards?: ActionCard[];
tokensUsed?: number;
modelUsed?: string;
}
/**
* 对话
*/
export interface ProtocolConversation {
id: string;
title: string;
createdAt: string;
updatedAt: string;
}
/**
* 方案生成请求
*/
export interface GenerateProtocolRequest {
conversationId: string;
options?: {
sections?: string[];
style?: 'academic' | 'concise';
};
}
/**
* 方案生成响应
*/
export interface GenerateProtocolResponse {
generationId: string;
status: 'generating' | 'completed';
content?: string;
estimatedTime?: number;
}

View File

@@ -208,6 +208,62 @@
transform: translate(2px, -2px);
}
/* === Indigo主题Protocol Agent 全流程) === */
.agent-card.theme-indigo {
background: linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 100%);
border-color: #C7D2FE;
border-width: 2px;
}
.agent-card.theme-indigo:hover {
border-color: #6366F1;
background: linear-gradient(135deg, #E0E7FF 0%, #C7D2FE 100%);
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.2);
}
.agent-card.theme-indigo .icon-box {
background-color: #6366F1;
border-color: #4F46E5;
}
.agent-card.theme-indigo .agent-icon {
color: #FFFFFF;
}
.agent-card.theme-indigo:hover .icon-box {
background-color: #4F46E5;
border-color: #4338CA;
transform: scale(1.08);
}
.agent-card.theme-indigo .num-watermark {
color: #C7D2FE;
}
.agent-card.theme-indigo:hover .num-watermark {
color: #A5B4FC;
}
/* Protocol Agent 特殊标记 */
.agent-card.protocol-agent-card {
position: relative;
}
.agent-card.protocol-agent-card::before {
content: '测试';
position: absolute;
top: 8px;
right: 8px;
font-size: 10px;
font-weight: 600;
color: #6366F1;
background: #E0E7FF;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid #C7D2FE;
}

View File

@@ -11,6 +11,7 @@
--brand-teal: #0D9488;
--brand-purple: #9333EA;
--brand-yellow: #CA8A04;
--brand-indigo: #6366F1;
}
/* === 整体布局 === */
@@ -182,6 +183,19 @@
background-color: var(--brand-purple);
}
.timeline-dot.theme-indigo {
background-color: var(--brand-indigo);
}
/* Protocol Agent 特殊样式 */
.timeline-dot.protocol-dot {
width: 28px;
height: 28px;
font-size: 14px;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
}
.timeline-line {
position: absolute;
top: 28px;
@@ -220,6 +234,31 @@
border-radius: 4px;
}
/* Beta徽章Protocol Agent */
.beta-badge {
font-size: 10px;
font-weight: 500;
color: var(--brand-indigo);
background-color: #EEF2FF;
border: 1px solid #C7D2FE;
padding: 2px 8px;
border-radius: 4px;
}
/* Protocol Agent 阶段标题 */
.protocol-title {
color: var(--brand-indigo);
}
/* Protocol Agent 网格(单列,宽度限制) */
.protocol-grid {
max-width: 340px;
}
.agents-grid.grid-cols-1 {
grid-template-columns: 1fr;
}
/* === 卡片网格 === */
.agents-grid {
display: grid;

View File

@@ -5,7 +5,7 @@
/**
* 智能体阶段主题色
*/
export type AgentTheme = 'blue' | 'yellow' | 'teal' | 'purple';
export type AgentTheme = 'blue' | 'yellow' | 'teal' | 'purple' | 'indigo';
/**
* 智能体配置
@@ -18,8 +18,9 @@ export interface AgentConfig {
theme: AgentTheme;
phase: number;
order: number;
isTool?: boolean; // 是否为工具类(跳转外部)
toolUrl?: string; // 工具跳转地址
isTool?: boolean; // 是否为工具类(跳转外部)
toolUrl?: string; // 工具跳转地址
isProtocolAgent?: boolean; // 是否为Protocol Agent全流程
}
/**
@@ -30,6 +31,7 @@ export interface PhaseConfig {
name: string;
theme: AgentTheme;
isTool?: boolean;
isProtocolAgent?: boolean; // 是否为Protocol Agent阶段
}
/**

View File

@@ -577,6 +577,8 @@ export default FulltextDetailDrawer;

View File

@@ -170,6 +170,8 @@ export const useAssets = (activeTab: AssetTabType) => {

View File

@@ -160,6 +160,8 @@ export const useRecentTasks = () => {

View File

@@ -130,6 +130,8 @@ export function useSessionStatus({

View File

@@ -122,6 +122,8 @@ export interface DataStats {

View File

@@ -118,6 +118,8 @@ export type AssetTabType = 'all' | 'processed' | 'raw';

View File

@@ -305,6 +305,8 @@ export default KnowledgePage;

View File

@@ -60,6 +60,8 @@ export interface BatchTemplate {

View File

@@ -138,6 +138,8 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm }: A

View File

@@ -58,6 +58,8 @@ export default function BatchToolbar({ selectedCount, onRunBatch, onClearSelecti

View File

@@ -81,6 +81,8 @@ export default function FilterChips({ filters, counts, onFilterChange }: FilterC

View File

@@ -71,6 +71,8 @@ export default function Header({ onUpload }: HeaderProps) {

View File

@@ -125,6 +125,8 @@ export default function ReportDetail({ report, onBack }: ReportDetailProps) {

View File

@@ -53,6 +53,8 @@ export default function ScoreRing({ score, size = 'medium', showLabel = true }:

View File

@@ -88,6 +88,8 @@ export default function Sidebar({ currentView, onViewChange, onSettingsClick }:

View File

@@ -30,6 +30,8 @@ export { default as TaskDetail } from './TaskDetail';

View File

@@ -299,6 +299,8 @@ export default function Dashboard() {

View File

@@ -248,6 +248,8 @@

View File

@@ -352,3 +352,5 @@ export default TenantListPage;

View File

@@ -261,3 +261,5 @@ export async function fetchModuleList(): Promise<ModuleInfo[]> {

View File

@@ -480,3 +480,5 @@ export default AIStreamChat;

View File

@@ -180,3 +180,5 @@ export default ConversationList;

View File

@@ -32,3 +32,5 @@ export type {

View File

@@ -324,3 +324,5 @@ export default useAIStream;

View File

@@ -253,3 +253,5 @@ export default useConversations;

View File

@@ -73,6 +73,8 @@ export { default as Placeholder } from './Placeholder';

View File

@@ -53,6 +53,8 @@ interface ImportMeta {