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:
78
frontend-v2/src/modules/aia/components/AgentCard.tsx
Normal file
78
frontend-v2/src/modules/aia/components/AgentCard.tsx
Normal 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;
|
||||
|
||||
143
frontend-v2/src/modules/aia/components/AgentHub.tsx
Normal file
143
frontend-v2/src/modules/aia/components/AgentHub.tsx
Normal 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>© 2025 临床研究平台 - 医学研究专属大模型</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentHub;
|
||||
|
||||
465
frontend-v2/src/modules/aia/components/ChatWorkspace.tsx
Normal file
465
frontend-v2/src/modules/aia/components/ChatWorkspace.tsx
Normal 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;
|
||||
|
||||
8
frontend-v2/src/modules/aia/components/index.ts
Normal file
8
frontend-v2/src/modules/aia/components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* AIA 模块组件导出
|
||||
*/
|
||||
|
||||
export { AgentHub } from './AgentHub';
|
||||
export { AgentCard } from './AgentCard';
|
||||
export { ChatWorkspace } from './ChatWorkspace';
|
||||
|
||||
Reference in New Issue
Block a user