feat(agent): add agent management feature with list view and routing

This commit is contained in:
2025-11-04 18:32:51 +08:00
parent 37dcab1597
commit b34988e830
15 changed files with 713 additions and 37 deletions

View File

@@ -0,0 +1,125 @@
# `agent` & `agents` 目录代码结构解析
本文档旨在详细解析 `ragflow_core_v0.21.1` 项目中 `src/pages/agent``src/pages/agents` 两个核心目录的结构、功能和关系。
- **`src/pages/agent`**: Agent/数据流的可视化编排器,是构建和调试单个 Agent 的工作区。
- **`src/pages/agents`**: Agent 的管理中心,负责列表展示、创建、模板管理和日志查看。
---
## 1. `src/pages/agent` - Agent 可视化编排器
此目录是整个 RAGFlow 的核心功能所在,提供了一个基于 `@xyflow/react` 的可视化画布用户可以通过拖拽节点和连接边的方式来构建复杂的数据处理流DSL
### 1.1. 目录结构概览
```
agent/
├── canvas/ # 画布核心组件
│ ├── node/ # 所有自定义节点的实现
│ └── edge/ # 自定义边的实现
├── form/ # 所有节点的配置表单
│ ├── agent-form/
│ └── ...
├── hooks/ # 画布相关的 Hooks
│ ├── use-build-dsl.ts
│ └── use-save-graph.ts
├── chat/ # 调试用的聊天面板
├── constant/ # Agent 相关的常量
├── index.tsx # Agent 页面主入口
└── store.ts # Zustand store管理画布状态
```
### 1.2. 关键子目录解析
#### `canvas/` - 画布与节点
- **`canvas/index.tsx`**: 画布主组件,负责整合节点、边、背景、小地图等,并处理拖拽、连接、删除等基本交互。
- **`canvas/node/`**: 定义了所有内置的节点类型。每个节点文件(如 `begin-node.tsx`, `retrieval-form/`) 负责节点的 UI 渲染、Handles连接点的定位和基本逻辑。
- `node-wrapper.tsx`: 为所有节点提供统一的包裹层,处理选中、错误、运行状态等通用 UI 逻辑。
- `card.tsx`: 节点内部内容的标准卡片布局。
- **`canvas/edge/`**: 自定义边的实现,可能包含特殊的路径、箭头或交互。
#### `form/` - 节点配置表单
当用户在画布上选中一个节点时,会弹出一个表单用于配置该节点的参数。此目录存放了所有节点对应的表单组件。
- 目录结构与节点类型一一对应,例如 `retrieval-form/` 对应检索节点。
- 每个表单组件负责:
1. 渲染配置项(如输入框、下拉框、开关等)。
2. 从节点数据中初始化表单。
3. 将用户的输入实时或在保存时更新回节点的 `data` 属性中。
- `form/components/` 包含了一些表单内复用的组件,如标准的输出配置 (`output.tsx`)。
#### `hooks/` - 核心逻辑 Hooks
此目录封装了画布的核心业务逻辑,使主页面 `index.tsx` 保持整洁。
- **`use-build-dsl.ts`**: 将画布上的节点和边Graph a object转换为后端可执行的领域特定语言DSL JSON。这是从前端图形表示到后端逻辑表示的关键转换。
- **`use-save-graph.ts`**: 负责保存当前的图结构(节点和边的位置、数据等)到后端。通常在用户手动点击保存或自动保存时触发。
- **`use-run-dataflow.ts`**: 触发当前 Agent 的运行,并处理返回的日志、结果等。
- **`use-set-graph.ts`**: 从后端获取图数据并将其渲染到画布上,用于加载已保存的 Agent。
#### `chat/`, `debug-content/`, `log-sheet/` - 调试与运行
- **`chat/`**: Agent 调试时使用的聊天界面,用于发送输入并实时查看 Agent 的回复。
- **`debug-content/`**: 调试抽屉的内容,可能包含更详细的输入/输出或日志信息。
- **`log-sheet/`**: 展示 Agent 运行历史日志的面板。
#### `store.ts`
基于 Zustand 的状态管理,全局维护画布的状态,例如:
- `nodes`, `edges`: 画布上的节点和边数组。
- `onNodesChange`, `onEdgesChange`: `@xyflow/react` 需要的回调。
- 当前选中的节点、画布的缩放/平移状态等。
- 其他 UI 状态,如配置面板是否打开。
---
## 2. `src/pages/agents` - Agent 管理中心
此目录负责管理所有的 Agent 实例,是用户与 Agent 交互的入口页面。
### 2.1. 目录结构概览
```
agents/
├── hooks/
│ ├── use-create-agent.ts
│ └── use-selelct-filters.ts
├── agent-card.tsx # 单个 Agent 的卡片展示
├── agent-log-page.tsx # Agent 运行日志列表页
├── agent-templates.tsx # Agent 模板页
├── create-agent-dialog.tsx # 创建 Agent 的对话框
├── index.tsx # Agent 列表页面主入口
└── use-rename-agent.ts # 重命名 Agent 的 Hook
```
### 2.2. 关键文件解析
- **`index.tsx`**: Agent 列表的主页面。通常会从后端获取所有 Agent 的列表,并使用 `agent-card.tsx` 将它们渲染出来。包含搜索、筛选和分页等功能。
- **`agent-card.tsx`**: 以卡片形式展示单个 Agent 的摘要信息,如名称、描述、更新时间等。点击卡片通常会导航到 `src/pages/agent` 页面,并传入对应的 Agent ID。
- **`create-agent-dialog.tsx` / `create-agent-form.tsx`**: 提供创建新 Agent 的表单和对话框。`use-create-agent.ts` Hook 封装了向后端发送创建请求的逻辑。
- **`agent-templates.tsx`**: 展示可供用户选择的预置 Agent 模板,帮助用户快速启动。
- **`agent-log-page.tsx`**: 展示所有 Agent 的历史运行日志,提供查询和筛选功能。点击某条日志可以查看详情。
- **`hooks/`**: 存放与 Agent 列表管理相关的业务逻辑。
- `use-create-agent.ts`: 封装创建 Agent 的 API 调用。
- `use-selelct-filters.ts`: 管理列表页的筛选条件。
---
## 3. 关系与数据流
1. **从 `agents` 到 `agent`**:
- 用户在 `agents` 列表页 (`/agents`) 点击一个 Agent 卡片。
- 应用导航到 `agent` 编排页 (`/agent/{agent_id}`)。
- `agent` 页面的 `use-set-graph.ts` Hook 被触发,使用 `agent_id` 从后端获取该 Agent 的图数据。
- 获取到的图数据通过 `store.ts` 设置到全局状态,画布 (`canvas/index.tsx`) 监听到状态变化后,渲染出对应的节点和边。
2. **从 `agent` 保存与返回**:
- 用户在 `agent` 页面修改了图结构。
- `use-save-graph.ts` Hook 将新的图结构保存到后端。
- `use-build-dsl.ts` 将图结构转换为 DSL 并一同保存。
- 用户返回 `agents` 列表页,可以看到 Agent 的更新时间等信息已变化。
这两个目录共同构成了一个完整的 Agent 创建、管理、编排和调试的闭环。

View File

@@ -1,23 +1,19 @@
import { Box, List, ListItemButton, ListItemText, Typography } from '@mui/material'; import { Box, List, ListItemButton, ListItemText, Typography } from '@mui/material';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined'; import {
import LibraryBooksOutlinedIcon from '@mui/icons-material/LibraryBooksOutlined'; LibraryBooksOutlined as KnowledgeBasesIcon,
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined'; AccountTreeOutlined as AgentIcon,
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; } from '@mui/icons-material';
import StorageOutlinedIcon from '@mui/icons-material/StorageOutlined'; import { useTranslation } from 'react-i18next';
import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined';
const navItems = [
// { text: 'Overview', path: '/', icon: DashboardOutlinedIcon },
{ text: 'Knowledge Bases', path: '/', icon: LibraryBooksOutlinedIcon },
// { text: 'RAG Pipeline', path: '/pipeline-config', icon: AccountTreeOutlinedIcon },
// { text: 'Operations', path: '/dashboard', icon: SettingsOutlinedIcon },
// { text: 'Models & Resources', path: '/models-resources', icon: StorageOutlinedIcon },
// { text: 'MCP', path: '/mcp', icon: ExtensionOutlinedIcon },
];
const Sidebar = () => { const Sidebar = () => {
const location = useLocation(); const location = useLocation();
const { t } = useTranslation();
const navItems = [
{ text: t('header.knowledgeBase'), path: '/knowledge', icon: KnowledgeBasesIcon },
{ text: t('header.agent'), path: '/agent', icon: AgentIcon },
];
return ( return (
<Box <Box
@@ -47,8 +43,10 @@ const Sidebar = () => {
<List> <List>
{navItems.map((item) => { {navItems.map((item) => {
const IconComponent = item.icon; const IconComponent = item.icon;
const isActive = location.pathname === item.path; let isActive = location.pathname === item.path;
if (item.path === '/knowledge' && location.pathname === '/') {
isActive = true;
}
return ( return (
<Link <Link
key={item.path} key={item.path}
@@ -84,7 +82,6 @@ const Sidebar = () => {
); );
})} })}
</List> </List>
<Box <Box
sx={{ sx={{
marginTop: 'auto', marginTop: 'auto',

View File

@@ -0,0 +1,125 @@
import React from 'react';
import {
Box,
Typography,
Card,
CardContent,
IconButton,
Avatar,
Chip,
} from '@mui/material';
import { MoreVert as MoreVertIcon } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import type { IFlow } from '@/interfaces/database/agent';
interface AgentCardProps {
agent: IFlow;
onMenuClick: (event: React.MouseEvent<HTMLElement>, agent: IFlow) => void;
onViewAgent: (agent: IFlow) => void;
}
const AgentCard: React.FC<AgentCardProps> = ({ agent, onMenuClick, onViewAgent }) => {
const { t } = useTranslation();
const getPermissionInfo = (permission: string) => {
switch (permission) {
case 'me':
return { label: t('common.private'), color: '#E3F2FD', textColor: '#1976D2' };
case 'team':
return { label: t('common.team'), color: '#E8F5E8', textColor: '#388E3C' };
default:
return { label: t('common.public'), color: '#FFF3E0', textColor: '#F57C00' };
}
};
const permissionInfo = getPermissionInfo(agent.permission || 'me');
const formatDate = (dateStr?: string) => {
if (!dateStr) return t('common.unknown');
return dateStr;
};
const nodeCount = agent.dsl?.graph?.nodes?.length ?? 0;
const edgeCount = agent.dsl?.graph?.edges?.length ?? 0;
return (
<Card sx={{ borderRadius: 2 }}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar src={agent.avatar} sx={{ bgcolor: 'primary.main' }}>{agent.title?.[0] || 'A'}</Avatar>
<Box>
<Typography variant="h6" fontWeight={600}>{agent.title || t('common.untitled')}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
{agent.canvas_category && (
<Chip label={agent.canvas_category} size="small" />
)}
<Chip
label={permissionInfo.label}
size="small"
sx={{ bgcolor: permissionInfo.color, color: permissionInfo.textColor }}
/>
</Box>
</Box>
</Box>
<Box>
<IconButton aria-label={t('common.more')} onClick={(e) => onMenuClick(e, agent)}>
<MoreVertIcon />
</IconButton>
</Box>
</Box>
<Typography
variant="body2"
color="text.secondary"
sx={{
mt: 1,
mb: 2,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{agent.description || t('common.noDescription')}
</Typography>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
mt: 2,
p: 1.5,
backgroundColor: '#F8F9FA',
borderRadius: 1,
}}
>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontSize: '1.25rem', fontWeight: 600, color: 'primary.main' }}>
{nodeCount}
</Typography>
<Typography variant="caption" color="text.secondary">{t('agent.nodes') || 'Nodes'}</Typography>
</Box>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontSize: '1.25rem', fontWeight: 600, color: 'primary.main' }}>
{edgeCount}
</Typography>
<Typography variant="caption" color="text.secondary">{t('agent.edges') || 'Edges'}</Typography>
</Box>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
{t('common.updatedAt') || 'Updated'}: {formatDate(agent.update_date)}
</Typography>
{agent.nickname && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
{t('knowledge.creator') || 'Creator'}: {agent.nickname}
</Typography>
)}
</CardContent>
</Card>
);
};
export default AgentCard;

View File

@@ -0,0 +1,125 @@
import React from 'react';
import { Box, Typography, Grid, Button, Menu, MenuItem } from '@mui/material';
import { ArrowForward as ArrowForwardIcon, Add as AddIcon } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import type { IFlow } from '@/interfaces/database/agent';
import AgentCard from './AgentCard';
interface AgentGridViewProps {
agents: IFlow[];
maxItems?: number;
showSeeAll?: boolean;
onSeeAll?: () => void;
onEdit?: (agent: IFlow) => void;
onDelete?: (agent: IFlow) => void;
onView?: (agent: IFlow) => void;
loading?: boolean;
searchTerm?: string;
onCreateAgent?: () => void;
}
const AgentGridView: React.FC<AgentGridViewProps> = ({
agents,
maxItems,
showSeeAll = false,
onSeeAll,
onEdit,
onDelete,
onView,
loading = false,
searchTerm = '',
onCreateAgent,
}) => {
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [selectedAgent, setSelectedAgent] = React.useState<IFlow | null>(null);
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, agent: IFlow) => {
setAnchorEl(event.currentTarget);
setSelectedAgent(agent);
};
const handleMenuClose = () => {
setAnchorEl(null);
setSelectedAgent(null);
};
const handleDelete = () => {
if (selectedAgent && onDelete) {
onDelete(selectedAgent);
}
handleMenuClose();
};
const handleView = () => {
if (selectedAgent && onView) {
onView(selectedAgent);
}
handleMenuClose();
};
const handleViewAgent = (agent: IFlow) => {
if (onView) {
onView(agent);
}
handleMenuClose();
};
const displayedAgents = maxItems ? agents.slice(0, maxItems) : agents;
const hasMore = maxItems && agents.length > maxItems;
if (loading) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography>{t('common.loading')}</Typography>
</Box>
);
}
if (agents.length === 0) {
return (
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary" gutterBottom>
{searchTerm ? (t('agent.noMatchingAgents') || 'No matching agents') : (t('agent.noAgents') || 'No agents')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{searchTerm ? (t('agent.tryAdjustingFilters') || 'Try adjusting filters') : (t('agent.createFirstAgent') || 'Create your first agent')}
</Typography>
{(!searchTerm && onCreateAgent) && (
<Button variant="contained" startIcon={<AddIcon />} onClick={onCreateAgent}>
{t('agent.createAgent') || 'Create Agent'}
</Button>
)}
</Box>
);
}
return (
<Box>
<Grid container spacing={3}>
{displayedAgents.map((agent) => (
<Grid key={agent.id} size={{ xs: 12, sm: 6, md: 4 }}>
<AgentCard agent={agent} onMenuClick={handleMenuClick} onViewAgent={handleViewAgent} />
</Grid>
))}
</Grid>
{showSeeAll && hasMore && (
<Box sx={{ textAlign: 'center', mt: 3 }}>
<Button variant="outlined" endIcon={<ArrowForwardIcon />} onClick={onSeeAll} sx={{ borderRadius: 2 }}>
{t('common.viewAll')} ({agents.length})
</Button>
</Box>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
<MenuItem onClick={handleView}>{t('common.viewDetails')}</MenuItem>
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
{t('common.delete')}
</MenuItem>
</Menu>
</Box>
);
};
export default AgentGridView;

97
src/constants/agent.ts Normal file
View File

@@ -0,0 +1,97 @@
export enum ProgrammingLanguage {
Python = 'python',
Javascript = 'javascript',
}
export const CodeTemplateStrMap = {
[ProgrammingLanguage.Python]: `def main(arg1: str, arg2: str) -> str:
return f"result: {arg1 + arg2}"
`,
[ProgrammingLanguage.Javascript]: `const axios = require('axios');
async function main({}) {
try {
const response = await axios.get('https://github.com/infiniflow/ragflow');
return 'Body:' + response.data;
} catch (error) {
return 'Error:' + error.message;
}
}`,
};
export enum AgentGlobals {
SysQuery = 'sys.query',
SysUserId = 'sys.user_id',
SysConversationTurns = 'sys.conversation_turns',
SysFiles = 'sys.files',
}
export enum AgentCategory {
AgentCanvas = 'agent_canvas',
DataflowCanvas = 'dataflow_canvas',
}
export enum AgentQuery {
Category = 'category',
}
export enum DataflowOperator {
Begin = 'File',
Note = 'Note',
Parser = 'Parser',
Tokenizer = 'Tokenizer',
Splitter = 'Splitter',
HierarchicalMerger = 'HierarchicalMerger',
Extractor = 'Extractor',
}
export enum Operator {
Begin = 'Begin',
Retrieval = 'Retrieval',
Categorize = 'Categorize',
Message = 'Message',
Relevant = 'Relevant',
RewriteQuestion = 'RewriteQuestion',
KeywordExtract = 'KeywordExtract',
Baidu = 'Baidu',
DuckDuckGo = 'DuckDuckGo',
Wikipedia = 'Wikipedia',
PubMed = 'PubMed',
ArXiv = 'ArXiv',
Google = 'Google',
Bing = 'Bing',
GoogleScholar = 'GoogleScholar',
DeepL = 'DeepL',
GitHub = 'GitHub',
BaiduFanyi = 'BaiduFanyi',
QWeather = 'QWeather',
ExeSQL = 'ExeSQL',
Switch = 'Switch',
WenCai = 'WenCai',
AkShare = 'AkShare',
YahooFinance = 'YahooFinance',
Jin10 = 'Jin10',
TuShare = 'TuShare',
Note = 'Note',
Crawler = 'Crawler',
Invoke = 'Invoke',
Email = 'Email',
Iteration = 'Iteration',
IterationStart = 'IterationItem',
Code = 'CodeExec',
WaitingDialogue = 'WaitingDialogue',
Agent = 'Agent',
Tool = 'Tool',
TavilySearch = 'TavilySearch',
TavilyExtract = 'TavilyExtract',
UserFillUp = 'UserFillUp',
StringTransform = 'StringTransform',
SearXNG = 'SearXNG',
Placeholder = 'Placeholder',
File = 'File', // pipeline
Parser = 'Parser',
Tokenizer = 'Tokenizer',
Splitter = 'Splitter',
HierarchicalMerger = 'HierarchicalMerger',
Extractor = 'Extractor',
Generate = 'Generate',
}

82
src/hooks/agent-hooks.ts Normal file
View File

@@ -0,0 +1,82 @@
import { useState, useCallback, useEffect } from 'react';
import agentService from '@/services/agent_service';
import type { IFlow } from '@/interfaces/database/agent';
import type { IAgentPaginationParams } from '@/interfaces/request/agent';
import logger from '@/utils/logger';
export interface UseAgentListState {
agents: IFlow[];
total: number;
loading: boolean;
error: string | null;
currentPage: number;
pageSize: number;
keywords: string;
orderby?: string;
desc?: boolean;
}
export interface UseAgentListReturn extends UseAgentListState {
fetchAgents: (params?: IAgentPaginationParams) => Promise<void>;
setKeywords: (keywords: string) => void;
setCurrentPage: (page: number) => void;
setPageSize: (size: number) => void;
setOrder: (orderby?: string, desc?: boolean) => void;
refresh: () => Promise<void>;
}
/**
* 智能体列表钩子
* @param initialParams 初始参数
*/
export const useAgentList = (initialParams?: IAgentPaginationParams) => {
const [agents, setAgents] = useState<IFlow[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(initialParams?.page || 1);
const [pageSize, setPageSize] = useState(initialParams?.page_size || 10);
const [keywords, setKeywords] = useState(initialParams?.keywords || '');
const fetchAgentList = useCallback(async (params?: IAgentPaginationParams) => {
setLoading(true);
setError(null);
try {
const response = await agentService.listCanvas(params);
const res = response.data || {};
logger.info('useAgentList fetchAgentList', res);
const data = res.data
setAgents(data.canvas || []);
setTotal(data.total || 0);
} catch (err: any) {
setError(err.message || 'Failed to fetch agent list');
} finally {
setLoading(false);
}
}, []);
const refresh = useCallback(() => fetchAgentList({
keywords,
page: currentPage,
page_size: pageSize,
}), [keywords, currentPage, pageSize]);
useEffect(() => {
refresh();
}, [refresh]);
return {
agents,
total,
loading,
error,
currentPage,
pageSize,
keywords,
fetchAgents: fetchAgentList,
setKeywords,
setCurrentPage,
setPageSize,
refresh,
};
};

View File

@@ -1,3 +1,13 @@
/**
* 分页请求参数
*/
export interface IAgentPaginationParams {
keywords?: string;
page?: number;
page_size?: number;
}
export interface IDebugSingleRequestBody { export interface IDebugSingleRequestBody {
component_id: string; component_id: string;
params: Record<string, any>; params: Record<string, any>;

View File

@@ -10,6 +10,7 @@ export interface IPaginationRequestBody {
* 分页请求参数 * 分页请求参数
*/ */
export interface IPaginationBody { export interface IPaginationBody {
keywords?: string;
page?: number; page?: number;
size?: number; size?: number;
} }

View File

@@ -278,7 +278,7 @@ export default {
setting: 'User settings', setting: 'User settings',
logout: 'Log out', logout: 'Log out',
fileManager: 'File Management', fileManager: 'File Management',
flow: 'Agent', agent: 'Agent',
search: 'Search', search: 'Search',
welcome: 'Welcome to', welcome: 'Welcome to',
}, },

View File

@@ -262,7 +262,7 @@ export default {
setting: '用户设置', setting: '用户设置',
logout: '登出', logout: '登出',
fileManager: '文件管理', fileManager: '文件管理',
flow: '智能体', agent: '智能体',
search: '搜索', search: '搜索',
welcome: '欢迎来到', welcome: '欢迎来到',
}, },

96
src/pages/agent/list.tsx Normal file
View File

@@ -0,0 +1,96 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
Box,
Typography,
TextField,
InputAdornment,
Paper,
Button,
Pagination,
Stack,
} from '@mui/material';
import { Search as SearchIcon, Refresh as RefreshIcon } from '@mui/icons-material';
import { useAgentList } from '@/hooks/agent-hooks';
import AgentGridView from '@/components/agent/AgentGridView';
function AgentListPage() {
const [searchValue, setSearchValue] = useState('');
const {
agents,
total,
loading,
currentPage,
pageSize,
setCurrentPage,
setKeywords,
refresh,
} = useAgentList({ page: 1, page_size: 10 });
const totalPages = useMemo(() => {
return Math.ceil((agents?.length || 0) / pageSize) || 1;
}, [agents, pageSize]);
const currentPageData = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return (agents || []).slice(startIndex, endIndex);
}, [agents, currentPage, pageSize]);
const handleSearch = useCallback(() => {
setKeywords(searchValue);
setCurrentPage(1);
}, [searchValue, setKeywords, setCurrentPage]);
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" fontWeight={600} mb={2}>Agent </Typography>
<Paper sx={{ p: 2, mb: 2 }}>
<Box display="flex" gap={2} alignItems="center">
<TextField
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
placeholder="搜索名称或描述"
size="small"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button variant="contained" onClick={handleSearch}></Button>
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={refresh}></Button>
</Box>
</Paper>
<AgentGridView
agents={currentPageData}
loading={loading}
searchTerm={searchValue}
/>
{totalPages >= 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
<Stack spacing={2}>
<Pagination
count={totalPages}
page={currentPage}
onChange={(_, page) => setCurrentPage(page)}
color="primary"
size="large"
showFirstButton
showLastButton
/>
<Typography variant="body2" color="text.secondary" textAlign="center">
{total} {currentPage} / {totalPages}
</Typography>
</Stack>
</Box>
)}
</Box>
);
}
export default AgentListPage;

View File

@@ -5,6 +5,7 @@ import Login from '../pages/login/Login';
import PipelineConfig from '../pages/PipelineConfig'; import PipelineConfig from '../pages/PipelineConfig';
import Dashboard from '../pages/Dashboard'; import Dashboard from '../pages/Dashboard';
import ModelsResources from '../pages/ModelsResources'; import ModelsResources from '../pages/ModelsResources';
import AgentList from '../pages/agent/list';
import { import {
KnowledgeBaseList, KnowledgeBaseList,
KnowledgeBaseCreate, KnowledgeBaseCreate,
@@ -32,7 +33,7 @@ const AppRoutes = () => {
{/* 使用MainLayout作为受保护路由的布局 */} {/* 使用MainLayout作为受保护路由的布局 */}
<Route path="/" element={<MainLayout />}> <Route path="/" element={<MainLayout />}>
{/* <Route index element={<Home />} /> */} <Route index element={<KnowledgeBaseList />} />
<Route path="knowledge"> <Route path="knowledge">
<Route index element={<KnowledgeBaseList />} /> <Route index element={<KnowledgeBaseList />} />
<Route path="create" element={<KnowledgeBaseCreate />} /> <Route path="create" element={<KnowledgeBaseCreate />} />
@@ -44,12 +45,9 @@ const AppRoutes = () => {
<Route path="overview" element={<KnowledgeLogsPage />} /> <Route path="overview" element={<KnowledgeLogsPage />} />
</Route> </Route>
</Route> </Route>
<Route index element={<KnowledgeBaseList />} /> <Route path="agent">
<Route path="pipeline-config" element={<PipelineConfig />} /> <Route index element={<AgentList />} />
<Route path="dashboard" element={<Dashboard />} /> </Route>
<Route path="models-resources" element={<ModelsResources />} />
<Route path="mcp" element={<MCP />} />
<Route path="form-field-test" element={<FormFieldTest />} />
</Route> </Route>
{/* 处理chunk相关路由 需要传入 kb_id doc_id */} {/* 处理chunk相关路由 需要传入 kb_id doc_id */}
<Route path="chunk"> <Route path="chunk">
@@ -68,7 +66,7 @@ const AppRoutes = () => {
</Route> </Route>
{/* 处理未匹配的路由 */} {/* 处理未匹配的路由 */}
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/knowledge" replace />} />
</Routes> </Routes>
); );
}; };

View File

@@ -0,0 +1,19 @@
import api from './api';
import request from '@/utils/request';
import type { IAgentPaginationParams } from '@/interfaces/request/agent';
/**
* 智能体服务
*/
const agentService = {
/**
* 获取团队下的Canvas列表
*/
listCanvas: (params?: IAgentPaginationParams) => {
return request.get(api.listTeamCanvas, { params });
},
};
export default agentService;

View File

@@ -151,6 +151,7 @@ export default {
// flow // flow
listTemplates: `${api_host}/canvas/templates`, listTemplates: `${api_host}/canvas/templates`,
listCanvas: `${api_host}/canvas/list`, listCanvas: `${api_host}/canvas/list`,
listTeamCanvas: `${api_host}/canvas/listteam`,
getCanvas: `${api_host}/canvas/get`, getCanvas: `${api_host}/canvas/get`,
getCanvasSSE: `${api_host}/canvas/getsse`, getCanvasSSE: `${api_host}/canvas/getsse`,
removeCanvas: `${api_host}/canvas/rm`, removeCanvas: `${api_host}/canvas/rm`,

View File

@@ -11,7 +11,7 @@
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": false,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
@@ -20,7 +20,7 @@
"strict": true, "strict": true,
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,