Files
deerflow-code/web-code/pages/AgentList.tsx
T
2026-05-28 18:29:37 +08:00

405 lines
17 KiB
TypeScript

// ============================================
// AgentList.tsx - 智能体列表页面组件
// ============================================
// 功能描述:展示系统中所有智能体的列表,支持搜索、筛选、分页等功能
// 主要特性:
// - 智能体卡片展示(名称、描述、模型类型、状态等)
// - 支持按类型和状态筛选
// - 支持搜索智能体名称
// - 分页功能
// - 暗黑/亮色主题切换
// - 查看、编辑、测试智能体操作
// ============================================
import React, { useState, useEffect, useRef } from 'react';
// 导入图标组件,用于界面中的各种图标显示
import { Plus, Search, Eye, Edit, Play, Pause, Trash2, Bot, ChevronRight, GitBranch } from 'lucide-react';
// 导入主题上下文,用于获取当前主题(暗黑/亮色)
import { useTheme } from '../contexts/ThemeContext';
// 导入页面类型定义,用于页面导航
import { PageType } from '../components/Sidebar';
// 导入请求服务,用于与后端 API 交互
import { requestCrudService } from '@/service/request-axios-service';
// 导入 TDesign 组件库的加载和分页组件
import { Loading, Pagination } from 'tdesign-react';
/**
* AgentItem - 智能体数据项接口定义
* 描述单个智能体的数据结构
*/
interface AgentItem {
avatarUrl: string; // 智能体头像 URL
createdAt: string; // 创建时间
description: string; // 智能体描述
id: string; // 智能体唯一标识符
modelName: string; // 使用的模型名称
name: string; // 智能体名称
systemPrompt: string; // 系统提示词
userId: string; // 创建者用户 ID
isDraft: boolean; // 是否为草稿状态
}
/**
* AgentListProps - AgentList 组件属性接口
* 定义组件接收的外部属性
*/
interface AgentListProps {
onViewDetail: (id: string) => void; // 查看智能体详情的回调函数
onNavigate: (page: PageType) => void; // 页面导航的回调函数
}
// 定义 API 基础 URL,从环境变量中获取,如果不存在则为空字符串
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
// 创建请求服务实例,用于后续 API 调用
const service = requestCrudService;
/**
* AgentList - 智能体列表组件
* 主组件函数,负责渲染智能体列表页面
* @param onViewDetail - 查看详情的回调函数
* @param onNavigate - 页面导航的回调函数
*/
const AgentList: React.FC<AgentListProps> = ({ onViewDetail, onNavigate }) => {
// 获取当前主题配置
const { theme } = useTheme();
// 判断是否为暗黑模式
const isDark = theme === 'dark';
// 智能体列表数据状态,存储所有智能体信息
const [agents, setAgents] = useState<AgentItem[]>([]);
const [searchQuery, setSearchQuery] = useState('');
// 加载状态,用于显示加载动画
const [loading, setLoading] = useState<boolean>(false);
// 当前页码,默认为第 1 页
const [pageNum, setPageNum] = useState<number>(1);
// 每页显示数量,默认为 10 条
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
// 数据总数,用于分页组件显示
const [total, setTotal] = useState<number>(0);
// 使用 ref 存储分页状态,避免组件重新渲染时丢失
const state = useRef({
pageNum: 1,
pageSize: 10
});
// 回车处理
const handleKeyDown = async (e) => {
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
e.preventDefault();
} else if (e.keyCode === 13) {
e.preventDefault();
await agentListSearch();
} else if (e.key === "/") {
e.preventDefault();
}
};
/**
* 根据智能体类型返回对应的标签颜色样式
* 根据当前主题(暗黑/亮色)返回不同的颜色配置
* @param category - 智能体类型(如 GLM-5、Qwen3.5-27B、叙事生成等)
* @returns 返回对应的 CSS 类名,用于标签背景色和文字颜色
*/
const getCategoryColor = (category: string) => {
// 暗黑模式下的颜色配置
if (isDark) {
switch (category) {
case 'GLM-5':
return 'bg-blue-500/20 text-blue-400'; // 蓝色背景 + 蓝色文字
case 'Qwen3.5-27B':
return 'bg-cyan-500/20 text-cyan-400'; // 青色背景 + 青色文字
case 'Qwen3-32B':
return 'bg-purple-500/20 text-purple-400'; // 紫色背景 + 紫色文字
case '叙事生成':
return 'bg-pink-500/20 text-pink-400'; // 粉色背景 + 粉色文字
case '逻辑链构建':
return 'bg-amber-500/20 text-amber-400'; // 琥珀色背景 + 琥珀色文字
case '风险评估':
return 'bg-red-500/20 text-red-400'; // 红色背景 + 红色文字
case '方案生成':
return 'bg-green-500/20 text-green-400'; // 绿色背景 + 绿色文字
default:
return 'bg-slate-500/20 text-slate-400'; // 默认灰色
}
} else {
// 亮色模式下的颜色配置,统一使用红色系
switch (category) {
case 'GLM-5':
return 'bg-red-100 text-red-600';
case 'Qwen3.5-27B':
return 'bg-red-100 text-red-600';
case 'Qwen3-32B':
return 'bg-red-100 text-red-600';
case '叙事生成':
return 'bg-red-100 text-red-600';
case '逻辑链构建':
return 'bg-red-100 text-red-600';
case '风险评估':
return 'bg-red-100 text-red-600';
case '方案生成':
return 'bg-red-100 text-red-600';
default:
return 'bg-gray-100 text-gray-600'; // 默认灰色
}
}
};
/**
* 根据智能体类型返回对应的图标颜色样式
* 用于智能体卡片左侧图标的背景色和图标颜色
* @param type - 智能体类型
* @returns 返回包含背景色(bg)和图标颜色(icon)的对象
*/
const getTypeIconColor = (type: string) => {
// 暗黑模式下的图标颜色配置
if (isDark) {
switch (type) {
case 'GLM-5':
return { bg: 'bg-blue-500/20', icon: 'text-blue-400' }; // 蓝色系
case 'Qwen3.5-27B':
return { bg: 'bg-cyan-500/20', icon: 'text-cyan-400' }; // 青色系
case 'Qwen3-32B':
return { bg: 'bg-pink-500/20', icon: 'text-pink-400' }; // 粉色系
case '校验类':
return { bg: 'bg-amber-500/20', icon: 'text-amber-400' }; // 琥珀色系
case '成稿类':
return { bg: 'bg-green-500/20', icon: 'text-green-400' }; // 绿色系
default:
return { bg: 'bg-slate-500/20', icon: 'text-slate-400' }; // 默认灰色
}
} else {
// 亮色模式下统一使用红色系
return { bg: 'bg-red-100', icon: 'text-red-500' };
}
};
/**
* 获取智能体列表数据
* 调用后端 API 获取智能体列表,并更新组件状态
* 请求参数包括页码、每页数量和搜索名称
*/
const agentListSearch = async () => {
// 构建请求参数对象
const parmy = {
pageNum: state.current.pageNum, // 当前页码
pageSize: state.current.pageSize, // 每页显示数量
name: searchQuery // 搜索名称(当前为空,可扩展)
}
// 开始加载,显示加载动画
setLoading(true);
// 调用后端 API 获取智能体列表数据
const res = await requestCrudService.post(`${zncmUrl}/agent/expertAgents/list`, parmy);
// 加载完成,隐藏加载动画
setLoading(false);
// 打印响应数据到控制台,用于调试
console.log('智能体列表', res);
// 解析响应数据
const result = res.data;
// 更新数据总数,用于分页
setTotal(result.total);
// 更新智能体列表数据,如果无数据则设置为空数组
setAgents(result.records || []);
}
/**
* 页码变化时的回调函数
* 当用户切换页码时触发,更新页码并重新获取数据
* @param value - 新的页码值
*/
const pageIndexChange = async (value: number) => {
// 更新 ref 中存储的页码值
state.current.pageNum = value;
// 更新页码状态,触发组件重新渲染
setPageNum(value);
// 重新获取智能体列表数据
await agentListSearch();
}
/**
* 每页显示数量变化时的回调函数
* 当用户切换每页显示条数时触发,更新每页数量并重新获取数据
* @param value - 新的每页显示数量
*/
const pageSizeChange = async (value: number) => {
// 更新 ref 中存储的每页显示数量
state.current.pageSize = value;
// 更新每页显示数量状态,触发组件重新渲染
setPageSize(value);
// 重新获取智能体列表数据
await agentListSearch();
}
/**
* 组件挂载时的副作用
* 组件首次渲染时自动调用一次获取智能体列表
* 依赖数组为空,表示只在组件挂载时执行一次
*/
useEffect(() => {
agentListSearch();
}, []);
return (
<div className="space-y-6">
{/* 页面标题区 */}
<div className="flex items-center justify-between">
<div>
<h1 className={`text-2xl font-bold mb-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>智能体配置管理</h1>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>统一管理系统中的智能体配置</p>
</div>
<button className={`flex items-center gap-2 px-4 py-2.5 text-white rounded-lg transition-colors shadow-md ${isDark
? 'bg-blue-600 hover:bg-blue-500'
: 'bg-red-600 hover:bg-red-500'
}`}>
<Plus className="w-4 h-4" />
<span className="text-sm font-medium">新建智能体</span>
</button>
</div>
{/* 搜索和筛选区 */}
<div className="flex items-center gap-4">
<div className="relative flex-1 max-w-md">
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-500' : 'text-red-400'}`} />
<input
type="text"
value={searchQuery}
onKeyDown={(e) => handleKeyDown(e)}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索智能体名称..."
className={`w-full pl-10 pr-4 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 transition-colors ${isDark
? 'bg-slate-800/50 border-blue-800/30 text-white placeholder-slate-500'
: 'bg-white border-red-200 text-gray-900 placeholder-red-300'
}`}
/>
</div>
<button className={`px-4 py-2.5 border rounded-lg text-sm transition-colors ${isDark
? 'bg-slate-800/50 border-blue-800/30 text-slate-300 hover:bg-slate-700/50'
: 'bg-white border-red-200 text-gray-700 hover:bg-red-50'
}`}>
全部类型
</button>
<button className={`px-4 py-2.5 border rounded-lg text-sm transition-colors ${isDark
? 'bg-slate-800/50 border-blue-800/30 text-slate-300 hover:bg-slate-700/50'
: 'bg-white border-red-200 text-gray-700 hover:bg-red-50'
}`}>
全部状态
</button>
</div>
{/* 分类标签 */}
<div className="flex items-center gap-3">
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>按类型筛选:</span>
{['编排类', '分析类', '叙事类', '校验类', '成稿类'].map((type) => (
<button
key={type}
className={`px-3 py-1.5 border rounded-full text-sm transition-colors ${isDark
? 'bg-slate-800/50 border-blue-800/30 text-slate-300 hover:bg-blue-600/20 hover:border-blue-500/50 hover:text-blue-400'
: 'bg-white border-red-200 text-gray-700 hover:bg-red-50 hover:border-red-300 hover:text-red-600'
}`}
>
{type}
</button>
))}
</div>
{/* 智能体列表 */}
<Loading loading={loading}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{agents.map((agent) => {
const typeColors = getTypeIconColor(agent.modelName);
return (
<div
key={agent.id}
className={`rounded-xl p-5 transition-all duration-300 group cursor-pointer border theme-card ${isDark
? 'bg-slate-800/30 backdrop-blur-xl border-blue-800/30 hover:border-blue-500/50'
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
}`}
onClick={() => {
onViewDetail(agent.id);
onNavigate('agent-detail');
}}
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${typeColors.bg}`}>
<Bot className={`w-6 h-6 ${typeColors.icon}`} />
</div>
<div>
<h3 className={`font-semibold text-lg group-hover:text-red-500 transition-colors ${isDark ? 'text-white' : 'text-gray-900'}`}>{agent.name}</h3>
<p className={`text-sm ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{agent.description}</p>
</div>
</div>
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${agent.isDraft
? isDark
? 'bg-green-500/20 text-green-400'
: 'bg-green-100 text-green-600'
: isDark
? 'bg-yellow-500/20 text-yellow-400'
: 'bg-yellow-100 text-yellow-600'
}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${agent.isDraft
? isDark ? 'bg-green-400' : 'bg-green-500'
: isDark ? 'bg-yellow-400' : 'bg-yellow-500'
}`} />
{agent.isDraft ? '启用' : '暂停'}
</span>
</div>
<div className="flex items-center gap-4 mb-4">
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${getCategoryColor(agent.modelName)}`}>
{agent.modelName}
</span>
{/* <span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{agent.description}</span> */}
</div>
<div className={`flex items-center justify-between pt-4 border-t ${isDark ? 'border-blue-800/30' : 'border-red-100'
}`}>
<div className="flex items-center gap-6">
<div className="overflow-hidden text-ellipsis line-clamp-3">
{/* <p className={`text-xs mb-1 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>输入字段</p> */}
<p className={`font-medium ${isDark ? 'text-cyan-400' : 'text-red-600'}`}>{agent.systemPrompt} 个</p>
</div>
{/* <div>
<p className={`text-xs mb-1 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>输出字段</p>
<p className={`font-medium ${isDark ? 'text-green-400' : 'text-red-600'}`}>{agent.outputs} 个</p>
</div> */}
</div>
<div className="flex items-center gap-2">
<button className={`p-2 rounded-lg transition-colors group/btn ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-red-50'
}`} title="查看">
<Eye className={`w-4 h-4 ${isDark ? 'text-slate-400 group-hover/btn:text-white' : 'text-red-400 group-hover/btn:text-red-600'}`} />
</button>
<button className={`p-2 rounded-lg transition-colors group/btn ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-red-50'
}`} title="编辑">
<Edit className={`w-4 h-4 ${isDark ? 'text-slate-400 group-hover/btn:text-white' : 'text-red-400 group-hover/btn:text-red-600'}`} />
</button>
<button className={`p-2 rounded-lg transition-colors group/btn ${isDark ? 'hover:bg-green-500/20' : 'hover:bg-green-50'
}`} title="测试">
<Play className={`w-4 h-4 ${isDark ? 'text-slate-400 group-hover/btn:text-green-400' : 'text-red-400 group-hover/btn:text-green-600'}`} />
</button>
</div>
</div>
</div>
);
})}
</div>
</Loading>
{/* 分页 */}
<div className={`flex items-center justify-between pt-4 ${isDark ? '' : 'border-t border-red-100'}`}>
<Pagination
total={total}
current={pageNum}
pageSize={pageSize}
onCurrentChange={(page) => pageIndexChange(Number(page))}
onPageSizeChange={(size) => pageSizeChange(Number(size))} // 每页数量变化时的回调
/>
</div>
</div>
);
};
export default AgentList;