feat(aia): Complete AIA V2.0 with universal streaming capabilities

Major Updates:
- Add StreamingService with OpenAI Compatible format (backend/common/streaming)
- Upgrade Chat component V2 with Ant Design X integration
- Implement AIA module with 12 intelligent agents
- Create AgentHub with 100% prototype V11 restoration
- Create ChatWorkspace with streaming response support
- Add ThinkingBlock for deep thinking display
- Add useAIStream Hook for OpenAI Compatible stream handling

Backend Common Capabilities (~400 lines):
- OpenAIStreamAdapter: SSE adapter with OpenAI format
- StreamingService: unified streaming service
- Support content and reasoning_content dual streams
- Deep thinking tag processing (<think>...</think>)

Frontend Common Capabilities (~2000 lines):
- AIStreamChat: modern streaming chat component
- ThinkingBlock: collapsible deep thinking display
- ConversationList: conversation management with grouping
- useAIStream: OpenAI Compatible stream handler Hook
- useConversations: conversation state management Hook
- Modern design styles (Ultramodern theme)

AIA Module Frontend (~1500 lines):
- AgentHub: 12 agent cards with timeline design
- ChatWorkspace: fullscreen immersive chat interface
- AgentCard: theme-colored cards (blue/yellow/teal/purple)
- 5 phases, 12 agents configuration
- Responsive layout (desktop + mobile)

AIA Module Backend (~900 lines):
- agentService: 12 agents config with system prompts
- conversationService: refactored with StreamingService
- attachmentService: file upload skeleton (30k token limit)
- 12 API endpoints with authentication
- Full CRUD for conversations and messages

Documentation:
- AIA module status and development guide
- Universal capabilities catalog (11 services)
- Quick reference card for developers
- System overview updates

Testing:
- Stream response verified (HTTP 200)
- Authentication working correctly
- Auto conversation creation working
- Deep thinking display working
- Message input and send working

Status: Core features completed (85%), attachment and history loading pending
This commit is contained in:
2026-01-14 19:09:28 +08:00
parent 4ed67a8846
commit 3d35e9c58b
38 changed files with 8448 additions and 335 deletions

View File

@@ -0,0 +1,78 @@
/**
* AgentCard - 智能体卡片组件
*
* 100%还原原型图V11的卡片设计
* - 背景色: #F6F9FF (蓝色系) / #F0FDFA (青色系-工具)
* - 边框: #E0E7FF / #CCFBF1
* - 圆角: 10px
* - 内边距: 14px
* - 最小高度: 145px
* - 序号水印
* - 悬停效果
*/
import React from 'react';
import * as LucideIcons from 'lucide-react';
import type { AgentConfig } from '../types';
import '../styles/agent-card.css';
interface AgentCardProps {
agent: AgentConfig;
onClick: (agent: AgentConfig) => void;
}
/**
* 动态获取 Lucide 图标
*/
const getIcon = (iconName: string): React.ComponentType<any> => {
// 转换为 PascalCase
const pascalCase = iconName
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
return (LucideIcons as any)[pascalCase] || LucideIcons.HelpCircle;
};
export const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick }) => {
const Icon = getIcon(agent.icon);
const orderStr = String(agent.order).padStart(2, '0');
const handleClick = () => {
if (agent.isTool && agent.toolUrl) {
// 工具类:跳转外部链接
window.location.href = agent.toolUrl;
} else {
onClick(agent);
}
};
return (
<div
className={`agent-card theme-${agent.theme} ${agent.isTool ? 'tool-card' : ''}`}
onClick={handleClick}
>
{/* 工具类:右上角跳转图标 */}
{agent.isTool && (
<LucideIcons.ExternalLink className="link-icon" />
)}
{/* 头部:图标 + 标题 + 序号 */}
<div className="card-header">
<div className="card-header-left">
<div className="icon-box">
<Icon className="agent-icon" />
</div>
<h3 className="card-title">{agent.name}</h3>
</div>
<span className="num-watermark">{orderStr}</span>
</div>
{/* 描述文本 */}
<p className="desc-text">{agent.description}</p>
</div>
);
};
export default AgentCard;

View File

@@ -0,0 +1,143 @@
/**
* AgentHub - 智能体大厅主界面
*
* 100%还原原型图V11
* - 最大宽度 760px居中
* - 头部搜索框
* - 时间轴设计(左侧序号圆点+连接线)
* - 5个阶段12个智能体卡片
*/
import React, { useState, useMemo } from 'react';
import { BrainCircuit, Search } from 'lucide-react';
import { AgentCard } from './AgentCard';
import { AGENTS, PHASES } from '../constants';
import type { AgentConfig } from '../types';
import '../styles/agent-hub.css';
interface AgentHubProps {
onAgentSelect: (agent: AgentConfig) => void;
}
export const AgentHub: React.FC<AgentHubProps> = ({ onAgentSelect }) => {
const [searchValue, setSearchValue] = useState('');
// 按阶段分组智能体
const agentsByPhase = useMemo(() => {
const grouped: Record<number, AgentConfig[]> = {};
AGENTS.forEach(agent => {
if (!grouped[agent.phase]) {
grouped[agent.phase] = [];
}
grouped[agent.phase].push(agent);
});
return grouped;
}, []);
// 搜索提交
const handleSearch = () => {
if (searchValue.trim()) {
// 默认进入第一个智能体并携带搜索内容
const firstAgent = AGENTS[0];
onAgentSelect({ ...firstAgent, initialQuery: searchValue } as any);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
}
};
return (
<div className="agent-hub">
{/* 主体内容 */}
<main className="hub-main">
{/* 头部搜索区 */}
<div className="hub-header">
<div className="header-title">
<div className="title-icon">
<BrainCircuit size={24} />
</div>
<h1 className="title-text">
<span className="title-badge">DeepSeek</span>
</h1>
</div>
<div className="search-box">
<input
type="text"
placeholder="输入研究想法例如SGLT2抑制剂对心衰患者预后的影响..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onKeyDown={handleKeyDown}
className="search-input"
/>
<button className="search-btn" onClick={handleSearch}>
<Search size={20} />
</button>
</div>
</div>
{/* 流水线模块 */}
<div className="pipeline-container">
{PHASES.map((phase, phaseIndex) => {
const isLast = phaseIndex === PHASES.length - 1;
const agents = agentsByPhase[phase.phase] || [];
// 根据阶段确定列数
let gridCols = 'grid-cols-3';
if (phase.phase === 2) gridCols = 'grid-cols-3'; // 4个卡片每行3个
if (phase.phase === 3) gridCols = 'grid-cols-3'; // 1个卡片
if (phase.phase === 4) gridCols = 'grid-cols-3'; // 2个卡片
if (phase.phase === 5) gridCols = 'grid-cols-3'; // 2个卡片
return (
<div
key={phase.phase}
className={`pipeline-stage ${isLast ? 'last-stage' : ''}`}
>
{/* 左侧时间轴 */}
<div className="timeline-marker">
<div className={`timeline-dot theme-${phase.theme}`}>
{phase.phase}
</div>
{!isLast && <div className="timeline-line" />}
</div>
{/* 阶段内容 */}
<div className="stage-content">
<h2 className="stage-title">
{phase.name}
{phase.isTool && (
<span className="tool-badge"></span>
)}
</h2>
<div className={`agents-grid ${gridCols}`}>
{agents.map(agent => (
<AgentCard
key={agent.id}
agent={agent}
onClick={onAgentSelect}
/>
))}
</div>
</div>
</div>
);
})}
</div>
</main>
{/* 底部 */}
<footer className="hub-footer">
<p>&copy; 2025 - </p>
</footer>
</div>
);
};
export default AgentHub;

View File

@@ -0,0 +1,465 @@
/**
* ChatWorkspace - 沉浸式对话工作台
*
* 参考原型图V2结合 Ant Design X 能力:
* - 左侧边栏:会话列表
* - 头部:智能体信息
* - 消息区:流式响应 + 深度思考
* - 输入区:附件上传
*/
import React, { useState, useCallback, useEffect, useRef } from 'react';
import {
ChevronLeft,
Plus,
Menu,
X,
Download,
Paperclip,
Lightbulb,
} from 'lucide-react';
import * as LucideIcons from 'lucide-react';
import { useAIStream } from '@/shared/components/Chat';
import { ThinkingBlock } from '@/shared/components/Chat';
import { getAccessToken } from '@/framework/auth/api';
import { AGENTS, AGENT_PROMPTS, BRAND_COLORS } from '../constants';
import type { AgentConfig, Conversation, Message } from '../types';
import '../styles/chat-workspace.css';
interface ChatWorkspaceProps {
agent: AgentConfig;
initialQuery?: string;
onBack: () => void;
}
/**
* 动态获取图标
*/
const getIcon = (iconName: string): React.ComponentType<any> => {
const pascalCase = iconName
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
return (LucideIcons as any)[pascalCase] || LucideIcons.Bot;
};
export const ChatWorkspace: React.FC<ChatWorkspaceProps> = ({
agent,
initialQuery,
onBack,
}) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [conversations, setConversations] = useState<Conversation[]>([]);
const [currentConversationId, setCurrentConversationId] = useState<string | null>(null);
const [isCreatingConversation, setIsCreatingConversation] = useState(false);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [deepThinkingEnabled, setDeepThinkingEnabled] = useState(true);
const messagesEndRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const AgentIcon = getIcon(agent.icon);
const themeColor = BRAND_COLORS[agent.theme];
// 获取欢迎语
const welcomePrompt = AGENT_PROMPTS[agent.id] || '有什么可以帮您的?';
// 流式响应 Hook仅在有对话时初始化
const {
content: streamContent,
thinking: streamThinking,
status: streamStatus,
isStreaming,
isThinking,
error: streamError,
sendMessage: sendStreamMessage,
abort,
} = useAIStream({
apiEndpoint: currentConversationId
? `/api/v1/aia/conversations/${currentConversationId}/messages/stream`
: '',
headers: {
Authorization: `Bearer ${getAccessToken()}`,
},
});
// 创建对话
const createConversation = useCallback(async () => {
if (isCreatingConversation) return null;
setIsCreatingConversation(true);
try {
const token = getAccessToken();
const response = await fetch('/api/v1/aia/conversations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
agentId: agent.id,
title: '新对话',
}),
});
if (!response.ok) {
throw new Error(`创建对话失败: ${response.status}`);
}
const result = await response.json();
const newConv: Conversation = {
id: result.data.id,
agentId: result.data.agentId,
title: result.data.title,
createdAt: new Date(result.data.createdAt),
updatedAt: new Date(result.data.updatedAt),
};
setConversations(prev => [newConv, ...prev]);
setCurrentConversationId(newConv.id);
return newConv.id;
} catch (error) {
console.error('[ChatWorkspace] 创建对话失败:', error);
return null;
} finally {
setIsCreatingConversation(false);
}
}, [agent.id, isCreatingConversation]);
// 初始化:自动创建对话
useEffect(() => {
if (!currentConversationId && !isCreatingConversation) {
createConversation();
}
}, [currentConversationId, isCreatingConversation, createConversation]);
// 滚动到底部
const scrollToBottom = useCallback(() => {
setTimeout(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, 100);
}, []);
// 切换侧边栏
const toggleSidebar = useCallback(() => {
setSidebarOpen(prev => !prev);
}, []);
// 新建对话
const handleNewChat = useCallback(async () => {
const convId = await createConversation();
if (convId) {
setMessages([]);
setInputValue('');
setSidebarOpen(false);
}
}, [createConversation]);
// 发送消息
const handleSend = useCallback(async () => {
const content = inputValue.trim();
if (!content || isStreaming) return;
// 确保有对话 ID
let convId = currentConversationId;
if (!convId) {
convId = await createConversation();
if (!convId) {
console.error('[ChatWorkspace] 创建对话失败');
return;
}
}
// 添加用户消息
const userMessage: Message = {
id: `user-${Date.now()}`,
role: 'user',
content,
createdAt: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInputValue('');
// 重置 textarea 高度
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
}
// 添加 AI 消息占位
const aiMessageId = `ai-${Date.now()}`;
const aiMessage: Message = {
id: aiMessageId,
role: 'assistant',
content: '',
thinking: '',
createdAt: new Date(),
};
setMessages(prev => [...prev, aiMessage]);
// 调用流式API
const result = await sendStreamMessage(content, {
conversationId: convId,
agentId: agent.id,
enableDeepThinking: deepThinkingEnabled,
});
// 更新对话标题
if (conversations.find(c => c.id === convId)?.title === '新对话') {
const title = content.length > 20 ? content.slice(0, 20) + '...' : content;
setConversations(prev =>
prev.map(c =>
c.id === convId
? { ...c, title, updatedAt: new Date() }
: c
)
);
}
}, [inputValue, isStreaming, currentConversationId, agent.id, deepThinkingEnabled, conversations, sendStreamMessage, createConversation]);
// 处理键盘事件
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}, [handleSend]);
// 自动调整 textarea 高度
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(e.target.value);
e.target.style.height = 'auto';
e.target.style.height = e.target.scrollHeight + 'px';
}, []);
// 更新流式消息
useEffect(() => {
if (streamContent || streamThinking) {
setMessages(prev => {
const lastMsg = prev[prev.length - 1];
if (lastMsg && lastMsg.role === 'assistant') {
return [
...prev.slice(0, -1),
{
...lastMsg,
content: streamContent,
thinking: streamThinking,
},
];
}
return prev;
});
}
}, [streamContent, streamThinking]);
// 消息变化时滚动
useEffect(() => {
scrollToBottom();
}, [messages, scrollToBottom]);
// 切换深度思考
const toggleDeepThinking = useCallback(() => {
setDeepThinkingEnabled(prev => !prev);
}, []);
return (
<div className="chat-workspace">
{/* 移动端遮罩 */}
{sidebarOpen && (
<div className="mobile-overlay" onClick={toggleSidebar} />
)}
{/* 左侧边栏 */}
<aside className={`workspace-sidebar ${sidebarOpen ? 'open' : ''}`}>
{/* 边栏头部 */}
<div className="sidebar-header">
<button className="back-btn" onClick={onBack}>
<ChevronLeft size={20} />
<span></span>
</button>
<button className="close-sidebar-btn" onClick={toggleSidebar}>
<X size={20} />
</button>
</div>
{/* 新建对话按钮 */}
<div className="sidebar-new">
<button className="new-chat-btn" onClick={handleNewChat}>
<Plus size={16} />
</button>
</div>
{/* 历史记录 */}
<div className="sidebar-history">
<div className="history-group">
<div className="history-label">Today</div>
{conversations.map(conv => (
<button
key={conv.id}
className={`history-item ${currentConversationId === conv.id ? 'active' : ''}`}
onClick={() => {
if (currentConversationId !== conv.id) {
setCurrentConversationId(conv.id);
setMessages([]); // 清空消息,后续可加载历史消息
setInputValue('');
}
setSidebarOpen(false);
}}
>
{conv.title}
</button>
))}
</div>
</div>
{/* 用户信息 */}
<div className="sidebar-user">
<div className="user-avatar">U</div>
<div className="user-info">
<div className="user-name">Dr. Wang</div>
<div className="user-plan"></div>
</div>
</div>
</aside>
{/* 主对话区 */}
<main className="workspace-main">
{/* 头部 */}
<header className="workspace-header">
<div className="header-left">
<button className="menu-btn" onClick={toggleSidebar}>
<Menu size={20} />
</button>
<div
className="agent-icon-box"
style={{ backgroundColor: themeColor }}
>
<AgentIcon size={20} color="white" />
</div>
<div className="agent-info">
<h2 className="agent-name">{agent.name}</h2>
<div className="agent-status">
<span className="status-dot" />
<span className="status-text">Online</span>
</div>
</div>
</div>
<div className="header-right">
<button className="header-action" title="导出对话">
<Download size={16} />
</button>
</div>
</header>
{/* 对话区域 - 自定义布局 */}
<div className="workspace-chat-container">
{/* 消息区域 */}
<div className="workspace-messages">
{/* 欢迎消息(左上角,类似消息气泡) */}
{messages.length === 0 && (
<div className="welcome-card-container">
<div className="welcome-icon" style={{ backgroundColor: themeColor }}>
<AgentIcon size={20} color="white" />
</div>
<div className="welcome-card">
<p className="welcome-text">{welcomePrompt}</p>
</div>
</div>
)}
{/* 加载中提示 */}
{!currentConversationId && isCreatingConversation && (
<div className="loading-container">
<div className="loading-spinner">...</div>
</div>
)}
{/* 消息列表 */}
{currentConversationId && messages.map((msg, index) => (
<div key={msg.id} className={`message-item ${msg.role}`}>
{msg.role === 'assistant' && (
<div className="message-avatar" style={{ backgroundColor: themeColor }}>
<AgentIcon size={20} color="white" />
</div>
)}
<div className="message-content-wrapper">
{/* 深度思考块 */}
{msg.role === 'assistant' && (msg.thinking || isThinking) && index === messages.length - 1 && (
<ThinkingBlock
content={streamThinking || msg.thinking || ''}
isThinking={isThinking}
defaultExpanded={true}
/>
)}
{/* 消息内容 */}
<div className={`message-bubble ${msg.role}`}>
{msg.content}
{msg.role === 'assistant' && isStreaming && index === messages.length - 1 && (
<span className="typing-cursor"></span>
)}
</div>
</div>
{msg.role === 'user' && (
<div className="message-avatar user-avatar">U</div>
)}
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* 输入区域(靠下) */}
<div className="workspace-input-area">
{/* 深度思考按钮 */}
<div className="input-toolbar">
<button
className={`deep-thinking-btn ${deepThinkingEnabled ? 'active' : ''}`}
onClick={toggleDeepThinking}
>
<Lightbulb size={14} />
</button>
<button className="attachment-btn">
<Paperclip size={16} />
</button>
</div>
{/* 输入框 */}
<div className="input-box">
<textarea
ref={textareaRef}
className="chat-textarea"
placeholder="输入问题,或使用 / 呼出快捷指令..."
rows={1}
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
disabled={isStreaming}
/>
<button
className="send-btn"
onClick={handleSend}
disabled={!inputValue.trim() || isStreaming}
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="m16 12-4-4-4 4"></path>
<path d="M12 16V8"></path>
</svg>
</button>
</div>
{/* 底部提示 */}
<p className="input-hint"> AI Markdown </p>
</div>
</div>
</main>
</div>
);
};
export default ChatWorkspace;

View File

@@ -0,0 +1,8 @@
/**
* AIA 模块组件导出
*/
export { AgentHub } from './AgentHub';
export { AgentCard } from './AgentCard';
export { ChatWorkspace } from './ChatWorkspace';