Files
deerflow-code/web-code/components/qa/AgentQA.tsx
T
2026-05-27 19:43:15 +08:00

579 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import {
Search,
History,
Bot,
Sparkles,
Database,
Layers,
FileText,
ChevronRight,
ChevronLeft,
Cpu,
Target,
Zap,
CheckCircle,
} from 'lucide-react';
import { useTheme } from '../../contexts/ThemeContext';
import { PageType } from '../../components/Sidebar';
import { ChatMessage, EmptyChatState, QAComposer, TypingIndicator } from './QACommon';
interface Agent {
id: string;
name: string;
category: string;
tags: string[];
description: string;
skills: string[];
dataSources: string[];
functions: string[];
outputFields: string[];
}
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
agentName?: string;
skills?: string[];
dataSources?: string[];
functions?: string[];
structuredOutput?: Record<string, any>;
}
/**
* 智能体问答组件
* 基于特定智能体的专业能力进行问答,支持选择不同领域的智能体
* @param onNavigate - 页面导航函数
*/
const AgentQA: React.FC<{ onNavigate: (page: PageType) => void }> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 模拟智能体列表
const agents: Agent[] = [
{
id: 'audience-agent',
name: 'AudienceAgent',
category: '受众分析',
tags: ['受众分层', '画像抽取', '行为预测'],
description: '专业的受众分析智能体,能够识别目标受众特征并生成画像',
skills: ['受众分层', '核心受众识别', '画像抽取', '行为预测'],
dataSources: ['知识库', 'Y系统', 'A系统'],
functions: ['信息抽取器', '标签聚类器', '结构化输出器'],
outputFields: ['core_audience', 'key_traits', 'barriers', 'opportunities'],
},
{
id: 'brand-agent',
name: 'BrandAgent',
category: '品牌分析',
tags: ['品牌定位', '传播策略', '调性分析'],
description: '专注品牌建设与传播策略的智能体',
skills: ['品牌定位分析', '传播路径规划', '品牌调性设计'],
dataSources: ['知识库', '行业数据库'],
functions: ['品牌诊断器', '策略生成器'],
outputFields: ['brand_position', 'communication_path', 'tone_guide'],
},
{
id: 'content-agent',
name: 'ContentAgent',
category: '内容创作',
tags: ['文案撰写', '创意生成', '多形式内容'],
description: '智能内容创作助手,支持多种形式的内容生成',
skills: ['文案撰写', '创意生成', '多形式内容', 'A/B测试文案'],
dataSources: ['知识库', '优秀案例库'],
functions: ['文案生成器', '风格适配器', '质量评估器'],
outputFields: ['headlines', 'body_copy', 'call_to_action', 'variants'],
},
{
id: 'channel-agent',
name: 'ChannelAgent',
category: '渠道规划',
tags: ['渠道选择', '投放策略', '效果评估'],
description: '专业的渠道分析与规划智能体',
skills: ['渠道选择', '投放策略规划', '效果评估', '预算分配'],
dataSources: ['Y系统', 'A系统', '行业数据'],
functions: ['渠道分析器', '投放优化器', 'ROI预测器'],
outputFields: ['recommended_channels', 'budget_allocation', 'expected_roi'],
},
];
// 状态管理
// 当前选中的智能体
const [selectedAgent, setSelectedAgent] = useState<Agent>(agents[0]);
// 智能体搜索关键词
const [searchQuery, setSearchQuery] = useState('');
// 聊天消息列表
const [messages, setMessages] = useState<Message[]>([]);
// 输入框内容
const [inputValue, setInputValue] = useState('');
// 加载状态
const [isLoading, setIsLoading] = useState(false);
// 左侧侧边栏折叠状态
const [isLeftSidebarCollapsed, setIsLeftSidebarCollapsed] = useState(false);
// 过滤智能体
const filteredAgents = agents.filter(agent =>
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
agent.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
agent.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
);
/**
* 处理发送消息
* 发送用户消息并模拟智能体响应
*/
const handleSend = () => {
if (!inputValue.trim()) return;
setIsLoading(true);
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: inputValue,
timestamp: new Date(),
};
setMessages([...messages, userMessage]);
setInputValue('');
// 模拟AI响应
setTimeout(() => {
const response: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: `基于${selectedAgent.name}的专业能力,我来为您分析:\n\n根据您的问题,我识别到核心受众特征如下:\n\n**核心受众识别**:\n- 主要年龄段:25-35岁\n- 职业特征:都市白领\n- 消费能力:中高端\n- 关注点:品质生活、个人成长\n\n**关键特质**:\n1. 追求个性化表达\n2. 注重社交认同\n3. 愿意为品质付费\n\n**可能存在的阻力**:\n- 价格敏感度较高\n- 对品牌忠诚度有限\n\n以上分析是否满足您的需求?如需调整维度,请继续补充信息。`,
timestamp: new Date(),
agentName: selectedAgent.name,
skills: selectedAgent.skills,
dataSources: selectedAgent.dataSources,
functions: selectedAgent.functions,
structuredOutput: {
core_audience: '25-35岁都市白领',
key_traits: ['追求个性化', '注重社交认同', '品质导向'],
barriers: ['价格敏感', '品牌忠诚度有限'],
opportunities: ['社交媒体影响大', '口碑传播效果好'],
},
};
setMessages(prev => [...prev, response]);
setIsLoading(false);
}, 1500);
};
/**
* 处理停止操作
* 停止当前正在进行的智能体响应
*/
const handleStop = () => {
setIsLoading(false);
};
/**
* 处理键盘事件
* 按Enter键发送消息(不按Shift)
*/
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
/**
* 保存问答记录
*/
const handleSaveRecord = () => {
alert('问答记录已保存');
};
/**
* 收藏当前对话
*/
const handleCollect = () => {
alert('已添加到收藏');
};
return (
<div className={`h-[calc(100vh-120px)] flex flex-col theme-bg rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
{/* 顶部工具栏 */}
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<div className="flex items-center gap-4">
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
智能体问答
</h1>
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-lg ${isDark ? 'bg-slate-700/50' : 'bg-red-50'}`}>
<Bot className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
当前智能体:
</span>
<span className={`text-sm font-medium ${isDark ? 'text-blue-400' : 'text-red-600'}`}>
{selectedAgent.name}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => onNavigate('qa-records')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
isDark
? 'bg-slate-700/50 text-slate-300 hover:bg-slate-600/50'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<History className="w-4 h-4" />
历史记录
</button>
</div>
</div>
{/* 主体内容区 */}
<div className="flex-1 flex overflow-hidden">
{/* 左侧智能体区 */}
<div className={`${isLeftSidebarCollapsed ? 'w-14 p-2' : 'w-72'} border-r flex-shrink-0 overflow-y-auto transition-all duration-300 ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'}`}>
<div className={`flex items-center ${isLeftSidebarCollapsed ? 'justify-center' : 'justify-between'} p-2 ${isLeftSidebarCollapsed ? '' : 'px-4 pt-4 pb-0'}`}>
{!isLeftSidebarCollapsed && (
<span className={`text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
智能体配置
</span>
)}
<button
type="button"
onClick={() => setIsLeftSidebarCollapsed(!isLeftSidebarCollapsed)}
className={`p-2 rounded-lg transition-all ${
isDark
? 'text-slate-400 hover:text-white hover:bg-slate-700/50'
: 'text-gray-500 hover:text-red-600 hover:bg-red-50'
}`}
title={isLeftSidebarCollapsed ? '展开侧边栏' : '收起侧边栏'}
>
{isLeftSidebarCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</button>
</div>
{!isLeftSidebarCollapsed && (<>
{/* 智能体搜索 */}
<div className="p-4">
<div className={`relative ${isDark ? 'bg-slate-700/50' : 'bg-white'} rounded-lg`}>
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索智能体..."
className={`w-full pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none ${
isDark
? 'bg-transparent text-white placeholder-slate-500'
: 'bg-transparent text-gray-800 placeholder-gray-400'
}`}
/>
</div>
</div>
{/* 智能体列表 */}
<div className="px-4 pb-4">
<h3 className={`text-xs font-medium uppercase tracking-wider mb-3 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
智能体列表
</h3>
<div className="space-y-2">
{filteredAgents.map((agent) => (
<button
key={agent.id}
onClick={() => setSelectedAgent(agent)}
className={`w-full text-left p-3 rounded-lg transition-all ${
selectedAgent.id === agent.id
? isDark
? 'bg-blue-600/20 border border-blue-500/50'
: 'bg-red-50 border border-red-200'
: isDark
? 'bg-slate-700/30 border border-transparent hover:bg-slate-700/50'
: 'bg-white border border-gray-200 hover:bg-gray-50'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
selectedAgent.id === agent.id
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'bg-slate-600 text-slate-300'
: 'bg-gray-100 text-gray-600'
}`}>
<Bot className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className={`text-sm font-medium truncate ${isDark ? 'text-white' : 'text-gray-900'}`}>
{agent.name}
</div>
<div className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{agent.category}
</div>
</div>
<ChevronRight className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
</div>
</button>
))}
</div>
</div>
{/* 当前智能体能力摘要 */}
<div className={`p-4 border-t ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<h3 className={`text-xs font-medium uppercase tracking-wider mb-3 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
能力摘要
</h3>
{/* 核心职责 */}
<div className="mb-4">
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Target className="w-4 h-4" />
<span className="text-xs">核心职责</span>
</div>
<p className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{selectedAgent.description}
</p>
</div>
{/* 已启用技能 */}
<div className="mb-4">
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Sparkles className="w-4 h-4" />
<span className="text-xs">已启用技能</span>
</div>
<div className="flex flex-wrap gap-1">
{selectedAgent.skills.map((skill, idx) => (
<span
key={idx}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
}`}
>
{skill}
</span>
))}
</div>
</div>
{/* 可访问数据源 */}
<div className="mb-4">
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Database className="w-4 h-4" />
<span className="text-xs">可访问数据源</span>
</div>
<div className="flex flex-wrap gap-1">
{selectedAgent.dataSources.map((source, idx) => (
<span
key={idx}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600'
}`}
>
{source}
</span>
))}
</div>
</div>
{/* 可调用功能 */}
<div>
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Layers className="w-4 h-4" />
<span className="text-xs">可调用功能</span>
</div>
<div className="flex flex-wrap gap-1">
{selectedAgent.functions.map((func, idx) => (
<span
key={idx}
className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
}`}
>
{func}
</span>
))}
</div>
</div>
</div>
</>)}
</div>
{/* 中间对话区 */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* 消息列表 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 ? (
<EmptyChatState
icon={<Bot className="w-11 h-11 opacity-60" />}
title={`开始与${selectedAgent.name}对话`}
description="基于智能体专业能力获取精准回答"
/>
) : (
messages.map((msg) => (
<ChatMessage
key={msg.id}
message={msg}
assistantLabel={msg.agentName || selectedAgent.name}
renderAssistantExtra={(assistantMessage) => (
<div className={`ml-12 p-3 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-gray-50'}`}>
<div className="mb-3">
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>本轮启用技能</h4>
<div className="flex flex-wrap gap-1">
{assistantMessage.skills?.map((skill, idx) => (
<span key={idx} className={`px-2 py-0.5 rounded text-xs ${isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'}`}>
{skill}
</span>
))}
</div>
</div>
<div className="mb-3">
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>本轮访问数据源</h4>
<div className="flex flex-wrap gap-1">
{assistantMessage.dataSources?.map((source, idx) => (
<span key={idx} className={`px-2 py-0.5 rounded text-xs ${isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600'}`}>
{source}
</span>
))}
</div>
</div>
<div className="mb-3">
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>本轮调用功能</h4>
<div className="flex flex-wrap gap-1">
{assistantMessage.functions?.map((func, idx) => (
<span key={idx} className={`px-2 py-0.5 rounded text-xs ${isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'}`}>
{func}
</span>
))}
</div>
</div>
{assistantMessage.structuredOutput && (
<div>
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>结构化输出</h4>
<div className={`p-3 rounded-lg ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
{Object.entries(assistantMessage.structuredOutput).map(([key, value]) => (
<div key={key} className="flex items-start gap-2 mb-1 last:mb-0">
<span className={`text-xs font-mono ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>{key}:</span>
<span className={`text-xs ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{Array.isArray(value) ? value.join(', ') : String(value)}
</span>
</div>
))}
</div>
</div>
)}
</div>
)}
/>
))
)}
{isLoading && <TypingIndicator label={`${selectedAgent.name} 正在调用能力生成回复`} assistantLabel={selectedAgent.name} />}
</div>
{/* 底部输入区 */}
<QAComposer
value={inputValue}
onChange={setInputValue}
onKeyDown={handleKeyDown}
onSubmit={handleSend}
onStop={handleStop}
onSave={handleSaveRecord}
onCollect={handleCollect}
isLoading={isLoading}
placeholder="输入问题或任务要求..."
showUpload={false}
/>
</div>
{/* 右侧执行详情区 */}
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
}`}>
<h3 className={`text-sm font-medium mb-4 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
执行详情
</h3>
{/* 本轮启用技能 */}
<div className="mb-6">
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Sparkles className="w-4 h-4" />
<span className="text-xs font-medium">本轮启用技能</span>
</div>
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-2">
{selectedAgent.skills.map((skill, idx) => (
<div key={idx} className="flex items-center gap-2">
<CheckCircle className={`w-3 h-3 ${isDark ? 'text-blue-400' : 'text-green-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{skill}</span>
</div>
))}
</div>
</div>
</div>
{/* 本轮访问数据源 */}
<div className="mb-6">
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Database className="w-4 h-4" />
<span className="text-xs font-medium">本轮访问数据源</span>
</div>
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-2">
{selectedAgent.dataSources.map((source, idx) => (
<div key={idx} className="flex items-center gap-2">
<Database className={`w-3 h-3 ${isDark ? 'text-green-400' : 'text-green-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{source}</span>
</div>
))}
</div>
</div>
</div>
{/* 本轮调用功能 */}
<div className="mb-6">
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<Layers className="w-4 h-4" />
<span className="text-xs font-medium">本轮调用功能</span>
</div>
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-2">
{selectedAgent.functions.map((func, idx) => (
<div key={idx} className="flex items-center gap-2">
<Cpu className={`w-3 h-3 ${isDark ? 'text-purple-400' : 'text-purple-500'}`} />
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{func}</span>
</div>
))}
</div>
</div>
</div>
{/* 结构化输出字段 */}
<div>
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
<FileText className="w-4 h-4" />
<span className="text-xs font-medium">输出字段</span>
</div>
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-2">
{selectedAgent.outputFields.map((field, idx) => (
<div key={idx} className="flex items-center gap-2">
<Zap className={`w-3 h-3 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
<span className={`text-xs font-mono ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>{field}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default AgentQA;