diff --git a/docs/agent-page-guide.md b/docs/agent-page-guide.md new file mode 100644 index 0000000..08bba5a --- /dev/null +++ b/docs/agent-page-guide.md @@ -0,0 +1,309 @@ +# Agent 页面功能与结构说明(ragflow_core_v0.21.1) + +本文档面向实现与维护,系统性梳理 `src/pages/agent` 的页面结构、数据流、关键组件与 Hook,以及常见功能点的改造入口,便于快速定位与扩展。 + +--- + +## 目录 + +- 入口与路由 +- 上下文与状态 +- 页面生命周期与数据加载 +- 画布层(渲染与交互) +- 表单层(节点配置) +- 保存与 DSL 构建 +- 运行与日志 +- 设置与头像 +- 常量与类型 +- 关键数据流(端到端) +- 常见功能点定位(修改入口) +- 报错与排查建议(头像 ERR_INVALID_URL) +- 扩展与实现建议 +- 快速定位清单 +- 附:新增节点类型操作指南 +- 附:运行与日志调用流程 +- 附:术语与约定 + +--- + +## 入口与路由 + +- `src/pages/agent/index.tsx` + - 页面主入口,渲染核心区域与抽屉/对话框: + - 画布:`AgentCanvas` + - 配置:`FormSheet` + - 调试运行:`RunSheet` + - 日志:`LogSheet` + - 版本:`VersionDialog` + - 嵌入:`EmbedDialog` + - 设置:`SettingDialog` + - 流水线:`PipelineRunSheet`、`PipelineLogSheet` + - 使用 `useParams` 读取 `agentId`;用 `useTranslation` 做国际化。 + - 使用 `useSetModalState` 管理抽屉/对话框开关;与多个 Hook 交互。 + +- `src/routes/index.tsx` + - 路由注册,包含 `AgentList` 和 `Agent` 详情入口。 + +--- + +## 上下文与状态 + +- `src/pages/agent/context.ts` + - `AgentFormContext`:表单上下文(当前节点配置、提交、重置)。 + - `AgentInstanceContext`:Agent 实例与持久化相关数据。 + - `AgentChatContext` / `AgentChatLogContext`:聊天与日志面板状态。 + - `HandleContext`:画布句柄事件(连接、拖拽、选择)。 + +- `src/pages/agent/store.ts`(Zustand + Immer) + - 全局画布状态:`nodes`、`edges`、选择状态、连接处理。 + - 常用方法:`addNode`、`updateNode`、`removeNode`、`addEdge`、`removeEdge`、`setNodes`、`setEdges`、`getNode`。 + - 用于画布交互与保存 DSL 的数据源。 + +--- + +## 页面生命周期与数据加载 + +- `src/pages/agent/hooks/use-fetch-data.ts` + - `useFetchDataOnMount`:组件挂载时加载 Agent 数据(含图信息),并调用 `useSetGraphInfo` 设置 `nodes`/`edges`。 + +- `src/pages/agent/hooks/use-set-graph.ts` + - `useSetGraphInfo(IGraph)`:批量设置 `nodes` 与 `edges`,用于从后端 DSL/存储中初始化画布。 + +--- + +## 画布层(渲染与交互) + +- `src/pages/agent/canvas/index.tsx` + - 基于 `ReactFlow` 的主画布组件,注册节点类型与边类型: + - 节点:如 `BeginNode`、`RagNode`、`GenerateNode`、`CategorizeNode` 等。 + - 边:`ButtonEdge`(包含自定义交互按钮)。 + - 交互逻辑: + - 连接(`onConnect`)、拖拽(`onDragStart`/`onDrop`)、选择与删除。 + - 与表单抽屉联动(选中节点时打开对应配置)。 + - 与调试运行、日志抽屉联动(运行前保存、展示日志)。 + - 与 Store 对接:读写 `nodes`、`edges`;对接上下文与 Hooks。 + +- 典型交互 Hook:`src/pages/agent/hooks/use-add-node.ts` + - `useInitializeOperatorParams`:各 `Operator` 的初始表单值(含 `llm_id` 注入)。 + - `useGetNodeName`:本地化节点名生成(依赖 i18n 文案键 `flow.*`)。 + - `useCalculateNewlyBackChildPosition`:新增子节点坐标计算(避免覆盖)。 + - `useAddChildEdge`:按句柄类型自动连边(例如右侧输出到目标节点 `End`)。 + - `useAddToolNode`:在 `Agent` 节点下添加 `Tool` 子节点,并限制工具节点唯一性与坐标位置。 + - 句柄约定:`NodeHandleId`(如 `End`、`Tool` 等),用于源/目标句柄命名与连边规则。 + +--- + +## 表单层(节点配置) + +- `src/pages/agent/form-sheet/next.tsx` + - `FormSheet`:节点配置侧边抽屉。 + - 根据当前选中节点类型渲染对应表单组件(通过类型映射)。 + - 支持节点重命名、单步调试入口、关闭重置。 + - 与 `AgentFormContext` 协作(提交、校验、状态)。 + +- `src/pages/agent/agent-form/index.tsx` + - `AgentForm`:Agent 节点核心配置: + - 系统提示与用户提示、LLM 设置(模型、温度、`top_p`、`max_tokens`)。 + - 消息历史窗口、重试次数、错误延迟、异常处理方法等。 + - 使用 `react-hook-form + zod` 进行表单状态与校验。 + - 表单保存与 DSL 映射在 `useSaveGraph` 中完成。 + +- 表单映射入口说明 + - 类型到表单组件的映射一般在 `FormSheet` 内部维护;若寻找 `form-config-map.ts` 未找到,建议在 `FormSheet` 内搜索类型映射来源或相关导入。 + +--- + +## 保存与 DSL 构建 + +- `src/pages/agent/hooks/use-save-graph.ts` + - `useSaveGraph`:将画布上的 `nodes` 与 `edges` 构建为 DSL 数据并提交后端保存 Agent(包含图与表单配置)。 + - `useSaveGraphBeforeOpeningDebugDrawer`:打开调试抽屉前保存图,并重置 Agent 实例数据。 + - `useWatchAgentChange`:监听 Agent 变化进行自动保存(防丢失)。 + +- `src/pages/agent/utils.ts` + - 工具方法:构建 Agent 异常跳转、判断上下游关系、Agent 工具分类、操作符参数转换等。 + - 与保存逻辑配合,确保 DSL 参数与 UI 表单一致。 + +--- + +## 运行与日志 + +- `src/pages/agent/hooks/use-run-dataflow.ts` + - `useRunDataflow`:运行前保存图、拉起日志面板;通过 SSE 发送运行请求,处理返回的消息 ID 与文件数据(如图像、附件)。 + +- `src/pages/agent/log-sheet/index.tsx` + - `LogSheet`:侧边日志面板,展示工作流时间线与事件列表(`WorkFlowTimeline`)。 + +- `src/pages/agent/hooks/use-fetch-pipeline-log.ts` + - `useFetchPipelineLog`:获取流水线日志,处理加载/完成状态与停止逻辑。 + +- `src/pages/agent/run-sheet/index.tsx` + - `RunSheet`:测试运行抽屉(集成 `DebugContent`),使用 `useSaveGraphBeforeOpeningDebugDrawer` 与 `useGetBeginNodeDataInputs` 获取初始输入。 + +- `src/pages/agent/pipeline-run-sheet/index.tsx` + - `PipelineRunSheet`:流水线运行面板,集成 `UploaderForm`(上传输入文件)。 + +--- + +## 设置与头像 + +- `src/pages/agent/setting-dialog/index.tsx` + - `SettingDialog`:Agent 设置保存对话框。 + - 处理头像文件转 base64,并随保存 payload 一并提交。 + +> 说明:如果后端截断 `data:image/png;base64,...` 导致前端加载 `ERR_INVALID_URL`,需要后端允许存储完整 base64 或改用文件存储 + URL 返回。前端转码逻辑本身是正确的。 + +--- + +## 常量与类型 + +- `src/pages/agent/options.ts` + - 常量选项,如 `LanguageOptions` 等。 + +- `src/pages/agent/interface.ts` + - 类型与接口定义:`IOperatorForm`、`IGenerateParameter`、`IInvokeVariable`、`IPosition`、`BeginQuery`、`IInputs` 等。 + +- `src/pages/agent/constant.ts`(在 `use-add-node.ts` 中引用) + - `Operator` 枚举与各节点初始值 `initial*Values`(如 `initialAgentValues`、`initialRetrievalValues` 等)。 + +--- + +## 关键数据流(端到端) + +1. 初始化 + - 进入详情页时,`useFetchDataOnMount` 拉取当前 Agent 的图与配置。 + - `useSetGraphInfo` 将后端返回的 `nodes`、`edges` 写入 `useGraphStore`。 + - 画布 `AgentCanvas` 根据 Store 渲染节点与边。 + +2. 编辑 + - 在画布上添加/连接节点(`use-add-node.ts` 管理初值与连边)。 + - 选中节点打开 `FormSheet`,填写并校验表单(如 `AgentForm`)。 + - 表单更改更新 Store 中的节点数据(通过上下文与 Hook)。 + +3. 保存 + - 点击保存或运行前,`useSaveGraph` 将 `nodes`/`edges` 转为 DSL 并提交后端。 + +4. 运行与日志 + - `useRunDataflow` 触发运行,后端通过 SSE 推送消息。 + - `LogSheet` 展示运行过程与结果,`PipelineRunSheet` 支持文件输入场景。 + +5. 设置与外层能力 + - `SettingDialog` 保存头像与显示配置;`VersionDialog` 管理版本;`EmbedDialog` 提供嵌入能力。 + +--- + +## 常见功能点定位(修改入口) + +- 画布行为 + - 新增节点初始参数:`hooks/use-add-node.ts` → `useInitializeOperatorParams` + - 节点命名与本地化:`useGetNodeName`(依赖 `locales` 文案键 `flow.*`) + - 子节点自动布局:`useCalculateNewlyBackChildPosition` + - 工具节点添加与唯一性限制:`useAddToolNode` + +- 保存与 DSL + - 转换/提交:`hooks/use-save-graph.ts`(调整 DSL 结构或保存字段) + - 参数转换与分类:`utils.ts` + +- 运行与日志 + - 运行触发与 SSE 处理:`hooks/use-run-dataflow.ts` + - 流水线日志轮询:`hooks/use-fetch-pipeline-log.ts` + - 时间线展示:`log-sheet/index.tsx` + +- 表单渲染 + - 抽屉入口与类型映射:`form-sheet/next.tsx`(及类型映射配置) + - Agent 节点表单字段与校验:`agent-form/index.tsx` + +- 全局状态 + - 节点/边增删改查:`store.ts` + - 初始图设置:`hooks/use-set-graph.ts` + +- 页面装配 + - 入口聚合与抽屉/对话框组织:`index.tsx` + +--- + +## 报错与排查建议(头像 ERR_INVALID_URL) + +- 问题表现:控制台出现对 `data:image/png;base64,...` 的 `GET`,报错 `ERR_INVALID_URL`。 +- 根因:后端字段长度或存储策略导致 Base64 被截断,前端加载失败。 +- 方案: + - 后端允许存储完整 Base64;或改用文件存储并返回 URL。 + - 前端无需改动转码逻辑,仅需按后端新策略使用头像 URL。 + +--- + +## 扩展与实现建议 + +- 新增节点类型 + - 在 `src/pages/agent/constant.ts` 增加 `Operator` 枚举与 `initial*Values`。 + - 在 `src/pages/agent/canvas/index.tsx` 注册节点组件。 + - 在 `src/pages/agent/form-sheet/next.tsx` 添加表单类型映射。 + - 在 `src/pages/agent/utils.ts` 中处理新节点 DSL 字段与参数转换。 + - 如需新表单组件:在 `src/pages/agent/agent-form/` 或对应目录编写组件并导出。 + +- 调整保存策略 + - 在 `useSaveGraph` 中扩展 DSL 构建、校验与错误提示。 + - `useWatchAgentChange` 可增加节流或差异保存,降低后端压力。 + +- 增强运行体验 + - 在 `useRunDataflow` 中处理更多事件类型与文件数据(图像/附件流转)。 + - 在 `LogSheet` 增加过滤与分组能力,提升可读性。 + +--- + +## 快速定位清单 + +- 入口渲染与抽屉组织:`src/pages/agent/index.tsx` +- 全局画布状态:`src/pages/agent/store.ts` +- 画布渲染与交互:`src/pages/agent/canvas/index.tsx` +- 初始化加载:`src/pages/agent/hooks/use-fetch-data.ts`、`src/pages/agent/hooks/use-set-graph.ts` +- 保存 DSL:`src/pages/agent/hooks/use-save-graph.ts` +- 运行数据流:`src/pages/agent/hooks/use-run-dataflow.ts` +- 日志与流水线:`src/pages/agent/log-sheet/index.tsx`、`src/pages/agent/pipeline-run-sheet/index.tsx`、`src/pages/agent/hooks/use-fetch-pipeline-log.ts` +- 表单抽屉:`src/pages/agent/form-sheet/next.tsx` +- Agent 表单:`src/pages/agent/agent-form/index.tsx` +- 工具与类型:`src/pages/agent/utils.ts`、`src/pages/agent/options.ts`、`src/pages/agent/interface.ts` +- 添加节点与连边:`src/pages/agent/hooks/use-add-node.ts` +- 列表页与基础查询:`src/hooks/agent-hooks.ts`、`src/pages/agent/list.tsx`(若存在) + +--- + +## 附:新增节点类型操作指南(示例) + +1. 在 `constant.ts` 中: + - 增加枚举:`Operator.MyNode` + - 增加初始值:`initialMyNodeValues` + +2. 在 `canvas/index.tsx` 中: + - 注册节点类型映射:`nodeTypes = { MyNode: MyNodeComponent, ... }` + +3. 在 `form-sheet/next.tsx` 中: + - 在类型映射里添加表单组件:`{ Operator.MyNode: MyNodeForm, ... }` + +4. 在 `utils.ts` 中: + - 扩展 DSL 构建逻辑,确保 `MyNode` 的参数能正确序列化到后端期望的字段。 + +5. 若需要服务端对接: + - 查看/扩展 `src/services/agent_service.ts` 中相关接口调用。 + +--- + +## 附:运行与日志调用流程(简化时序) + +1. 点击运行 → `useRunDataflow` +2. 调 `useSaveGraph` 保存当前图(DSL) +3. 发送运行请求(SSE)到后端 +4. 打开 `LogSheet` 并持续接收事件 +5. 显示工作流时间线与每步输出(含文件数据) + +--- + +## 附:术语与约定 + +- `Operator`:画布节点的类型枚举。 +- `NodeHandleId`:节点句柄标识,控制连边的输入输出端(如 `End`、`Tool`)。 +- `DSL`:后端可执行的流程定义 JSON,由画布 `nodes`/`edges` 转换而来。 + +--- + +如需我导出一份“节点 → DSL 字段”对照表或补充类型映射的完整清单,请告诉我具体节点范围,我将进一步扫描相关目录并生成结构化手册,帮助你在实现新节点或对接后端时做到“改哪里、加什么、传什么”一目了然。 \ No newline at end of file diff --git a/src/components/agent/AgentCard.tsx b/src/components/agent/AgentCard.tsx index cb8a847..3c89d31 100644 --- a/src/components/agent/AgentCard.tsx +++ b/src/components/agent/AgentCard.tsx @@ -11,6 +11,7 @@ import { import { MoreVert as MoreVertIcon } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; import type { IFlow } from '@/interfaces/database/agent'; +import dayjs from 'dayjs'; interface AgentCardProps { agent: IFlow; @@ -21,6 +22,17 @@ interface AgentCardProps { const AgentCard: React.FC = ({ agent, onMenuClick, onViewAgent }) => { const { t } = useTranslation(); + const getAvatarSrc = (src?: string) => { + if (!src) return undefined; + // Already a valid data URL + if (/^data:image\/(png|jpeg|jpg|webp);base64,/.test(src)) return src; + // HTTP(S) URL + if (/^https?:\/\//.test(src)) return src; + // Raw base64 without header -> assume png + if (/^[A-Za-z0-9+/=]+$/.test(src)) return `data:image/png;base64,${src}`; + return src; + }; + const getPermissionInfo = (permission: string) => { switch (permission) { case 'me': @@ -34,26 +46,18 @@ const AgentCard: React.FC = ({ agent, onMenuClick, onViewAgent } 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 ( - + - {agent.title?.[0] || 'A'} + - {agent.title || t('common.untitled')} + {agent.title} - {agent.canvas_category && ( - - )} = ({ agent, onMenuClick, onViewAgent } - {agent.description || t('common.noDescription')} + {agent.description || '-'} = ({ agent, onMenuClick, onViewAgent } - {t('common.updatedAt') || 'Updated'}: {formatDate(agent.update_date)} + {t('agent.updatedAt')}: {dayjs(agent.update_date).format('YYYY-MM-DD HH:mm:ss')} {agent.nickname && ( - {t('knowledge.creator') || 'Creator'}: {agent.nickname} + {t('agent.creator')}: {agent.nickname} )} diff --git a/src/components/agent/AgentGridView.tsx b/src/components/agent/AgentGridView.tsx index eac7d6a..55d9cdf 100644 --- a/src/components/agent/AgentGridView.tsx +++ b/src/components/agent/AgentGridView.tsx @@ -51,6 +51,13 @@ const AgentGridView: React.FC = ({ handleMenuClose(); }; + const handleEdit = () => { + if (selectedAgent && onEdit) { + onEdit(selectedAgent); + } + handleMenuClose(); + }; + const handleView = () => { if (selectedAgent && onView) { onView(selectedAgent); @@ -80,14 +87,14 @@ const AgentGridView: React.FC = ({ return ( - {searchTerm ? (t('agent.noMatchingAgents') || 'No matching agents') : (t('agent.noAgents') || 'No agents')} + {searchTerm ? (t('agent.noMatchingAgents')) : (t('agent.noAgents'))} - {searchTerm ? (t('agent.tryAdjustingFilters') || 'Try adjusting filters') : (t('agent.createFirstAgent') || 'Create your first agent')} + {searchTerm ? (t('agent.tryAdjustingFilters')) : (t('agent.createFirstAgent'))} {(!searchTerm && onCreateAgent) && ( )} @@ -113,9 +120,11 @@ const AgentGridView: React.FC = ({ )} - {t('common.viewDetails')} + {onEdit && ( + {t('agent.editAgent')} + )} - {t('common.delete')} + {t('agent.deleteAgent')} diff --git a/src/constants/common.ts b/src/constants/common.ts index cacd95f..840c746 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -150,15 +150,15 @@ export const ExceptiveType = ['xlsx', 'xls', 'pdf', 'docx', ...Images]; export const SupportedPreviewDocumentTypes = [...ExceptiveType]; //#endregion -// export enum Platform { -// RAGFlow = 'RAGFlow', -// Dify = 'Dify', -// FastGPT = 'FastGPT', -// Coze = 'Coze', -// } +export enum Platform { + RAGFlow = 'RAGFlow', + Dify = 'Dify', + FastGPT = 'FastGPT', + Coze = 'Coze', +} -// export enum ThemeEnum { -// Dark = 'dark', -// Light = 'light', -// System = 'system', -// } +export enum ThemeEnum { + Dark = 'dark', + Light = 'light', + System = 'system', +} diff --git a/src/hooks/agent-hooks.ts b/src/hooks/agent-hooks.ts index 2d7a994..4f039be 100644 --- a/src/hooks/agent-hooks.ts +++ b/src/hooks/agent-hooks.ts @@ -1,8 +1,10 @@ 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 type { IAgentPaginationParams, IAgentCreateRequestBody, IAgentSettingRequestBody } from '@/interfaces/request/agent'; import logger from '@/utils/logger'; +import { useTranslation } from 'react-i18next'; +import { useSnackbar } from '@/hooks/useSnackbar'; export interface UseAgentListState { agents: IFlow[]; @@ -79,4 +81,80 @@ export const useAgentList = (initialParams?: IAgentPaginationParams) => { setPageSize, refresh, }; -}; \ No newline at end of file +}; + + +export function useAgentOperations() { + const { t } = useTranslation(); + const { showMessage } = useSnackbar(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const createAgent = useCallback(async (body: IAgentCreateRequestBody) => { + try { + setLoading(true); + setError(null); + const res = await agentService.setCanvas(body); + const newId = res?.data?.data?.id || res?.data?.id; + showMessage.success(t('agent.createAgentSuccess')); + return { success: true, id: newId } as const; + } catch (err: any) { + const errorMessage = err?.response?.data?.message || err?.message || t('agent.createAgentFailed'); + setError(errorMessage); + showMessage.error(errorMessage); + return { success: false, error: errorMessage } as const; + } finally { + setLoading(false); + } + }, [t, showMessage]); + + const editAgent = useCallback(async (data: Partial) => { + try { + setLoading(true); + setError(null); + const res = await agentService.settingAgent(data); + const code = res?.data?.code; + if (code === 0) { + showMessage.success(t('message.updated')); + return { success: true } as const; + } + const msg = res?.data?.message || t('common.operationFailed'); + showMessage.error(msg); + return { success: false, error: msg } as const; + } catch (err: any) { + const errorMessage = err?.response?.data?.message || err?.message || t('common.operationFailed'); + setError(errorMessage); + showMessage.error(errorMessage); + return { success: false, error: errorMessage } as const; + } finally { + setLoading(false); + } + }, [t, showMessage]); + + const deleteAgents = useCallback(async (ids: string[]) => { + try { + setLoading(true); + setError(null); + const res = await agentService.removeCanvas(ids); + const code = res?.data?.code; + if (code === 0) { + showMessage.success(t('message.deleted')); + return { success: true } as const; + } + const msg = res?.data?.message || t('common.operationFailed'); + showMessage.error(msg); + return { success: false, error: msg } as const; + } catch (err: any) { + const errorMessage = err?.response?.data?.message || err?.message || t('common.operationFailed'); + setError(errorMessage); + showMessage.error(errorMessage); + return { success: false, error: errorMessage } as const; + } finally { + setLoading(false); + } + }, [t, showMessage]); + + const clearError = useCallback(() => setError(null), []); + + return { loading, error, createAgent, editAgent, deleteAgents, clearError }; +} \ No newline at end of file diff --git a/src/interfaces/database/agent.ts b/src/interfaces/database/agent.ts index 092776b..ad5ecd5 100644 --- a/src/interfaces/database/agent.ts +++ b/src/interfaces/database/agent.ts @@ -63,6 +63,7 @@ export interface IOperatorNode { export declare interface IFlow { avatar?: string; canvas_type: null; + canvas_category?: string; create_date: string; create_time: number; description: null; @@ -75,7 +76,6 @@ export declare interface IFlow { permission: string; nickname: string; operator_permission: number; - canvas_category: string; } export interface IFlowTemplate { diff --git a/src/interfaces/database/flow.ts b/src/interfaces/database/flow.ts deleted file mode 100644 index 6174ce6..0000000 --- a/src/interfaces/database/flow.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { Edge, Node } from '@xyflow/react'; -import type { IReference, Message } from './chat'; - -export type DSLComponents = Record; - -export interface DSL { - components: DSLComponents; - history: any[]; - path?: string[][]; - answer?: any[]; - graph?: IGraph; - messages: Message[]; - reference: IReference[]; - globals: Record; - retrieval: IReference[]; -} - -export interface IOperator { - obj: IOperatorNode; - downstream: string[]; - upstream: string[]; - parent_id?: string; -} - -export interface IOperatorNode { - component_name: string; - params: Record; -} - -export declare interface IFlow { - avatar?: string; - canvas_type: null; - create_date: string; - create_time: number; - description: string; - dsl: DSL; - id: string; - title: string; - update_date: string; - update_time: number; - user_id: string; - permission: string; - nickname: string; -} - -export interface IFlowTemplate { - avatar: string; - canvas_type: string; - create_date: string; - create_time: number; - description: { - en: string; - zh: string; - }; - dsl: DSL; - id: string; - title: { - en: string; - zh: string; - }; - update_date: string; - update_time: number; -} - -export type ICategorizeItemResult = Record< - string, - Omit ->; - -export interface IGenerateForm { - max_tokens?: number; - temperature?: number; - top_p?: number; - presence_penalty?: number; - frequency_penalty?: number; - cite?: boolean; - prompt: number; - llm_id: string; - parameters: { key: string; component_id: string }; -} -export interface ICategorizeItem { - name: string; - description?: string; - examples?: string; - to?: string; - index: number; -} - -export interface ICategorizeForm extends IGenerateForm { - category_description: ICategorizeItemResult; -} - -export interface IRelevantForm extends IGenerateForm { - yes: string; - no: string; -} - -export interface ISwitchCondition { - items: ISwitchItem[]; - logical_operator: string; - to: string[] | string; -} - -export interface ISwitchItem { - cpn_id: string; - operator: string; - value: string; -} - -export interface ISwitchForm { - conditions: ISwitchCondition[]; - end_cpn_id: string; - no: string; -} - -export interface IBeginForm { - prologue?: string; -} - -export interface IRetrievalForm { - similarity_threshold?: number; - keywords_similarity_weight?: number; - top_n?: number; - top_k?: number; - rerank_id?: string; - empty_response?: string; - kb_ids: string[]; -} - -export interface ICodeForm { - inputs?: Array<{ name?: string; component_id?: string }>; - lang: string; - script?: string; -} - -export type BaseNodeData = { - label: string; // operator type - name: string; // operator name - color?: string; - form?: TForm; -}; - -export type BaseNode = Node>; - -export type IBeginNode = BaseNode; -export type IRetrievalNode = BaseNode; -export type IGenerateNode = BaseNode; -export type ICategorizeNode = BaseNode; -export type ISwitchNode = BaseNode; -export type IRagNode = BaseNode; -export type IRelevantNode = BaseNode; -export type ILogicNode = BaseNode; -export type INoteNode = BaseNode; -export type IMessageNode = BaseNode; -export type IRewriteNode = BaseNode; -export type IInvokeNode = BaseNode; -export type ITemplateNode = BaseNode; -export type IEmailNode = BaseNode; -export type IIterationNode = BaseNode; -export type IIterationStartNode = BaseNode; -export type IKeywordNode = BaseNode; -export type ICodeNode = BaseNode; -export type IAgentNode = BaseNode; - -export type RAGFlowNodeType = - | IBeginNode - | IRetrievalNode - | IGenerateNode - | ICategorizeNode - | ISwitchNode - | IRagNode - | IRelevantNode - | ILogicNode - | INoteNode - | IMessageNode - | IRewriteNode - | IInvokeNode - | ITemplateNode - | IEmailNode - | IIterationNode - | IIterationStartNode - | IKeywordNode; - -export interface IGraph { - nodes: RAGFlowNodeType[]; - edges: Edge[]; -} diff --git a/src/interfaces/request/agent.ts b/src/interfaces/request/agent.ts index 61c9224..c08b392 100644 --- a/src/interfaces/request/agent.ts +++ b/src/interfaces/request/agent.ts @@ -1,3 +1,5 @@ +import { DSL } from "../database/agent"; + /** * 分页请求参数 */ @@ -12,3 +14,26 @@ export interface IDebugSingleRequestBody { component_id: string; params: Record; } + +export interface IAgentCreateRequestBody { + title: string; + avatar?: string; + description?: string | { zh?: string; en?: string }; + dsl: DSL; + canvas_category: string; +} + +export interface IAgentSettingRequestBody { + avatar?: string; + canvas_category?: string; + description?: string; + id: string; + title: string; + permission: string; +} + +export interface IAgentSetDSLRequestBody { + id: string; + title: string; + dsl: DSL; +} diff --git a/src/interfaces/request/flow.ts b/src/interfaces/request/flow.ts deleted file mode 100644 index 0ee8bcd..0000000 --- a/src/interfaces/request/flow.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IDebugSingleRequestBody { - component_id: string; - params: any[]; -} diff --git a/src/locales/en.ts b/src/locales/en.ts index dc778a3..386307f 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -66,6 +66,7 @@ export default { moreActions: 'More Actions', disable: 'Disable', enable: 'Enable', + onlyMe: 'Only Me', team: 'Team', public: 'Public', unknown: 'Unknown', @@ -1211,6 +1212,33 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s 'If your model provider is not listed but claims to be "OpenAI-compatible", select the OpenAI-API-compatible card to add the relevant model(s). ', mcp: 'MCP', }, + agent: { + agentList: 'Agent List', + noMatchingAgents: 'No matching agents', + noAgents: 'No agents', + tryAdjustingFilters: 'Try adjusting your filters', + createFirstAgent: 'Create your first agent', + createAgent: 'Create agent', + updatedAt: 'Updated At', + creator: 'Creator', + nodes: 'Nodes', + edges: 'Edges', + editAgent: 'Edit Agent', + deleteAgent: 'Delete Agent', + + forms: { + title: 'Title', + avatar: 'Avatar', + description: 'Description', + permissionSettings: 'Permission Settings', + }, + + // create agent + useTemplate: 'Use Template', + createAgentSuccess: 'Agent created successfully', + createAgentFailed: 'Agent creation failed', + + }, message: { registered: 'Registered!', logout: 'logout', @@ -1345,151 +1373,6 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s total: 'Total {{total}}', page: '{{page}} /Page', }, - dataflowParser: { - parseSummary: 'Parse Summary', - parseSummaryTip: 'Parser:deepdoc', - rerunFromCurrentStep: 'Rerun From Current Step', - rerunFromCurrentStepTip: 'Changes detected. Click to re-run.', - confirmRerun: 'Confirm Rerun Process', - confirmRerunModalContent: ` -

- You are about to rerun the process starting from the {{step}} step. -

-

This will:

-
    -
  • Overwrite existing results from the current step onwards
  • -
  • Create a new log entry for tracking
  • -
  • Previous steps will remain unchanged
  • -
`, - changeStepModalTitle: 'Step Switch Warning', - changeStepModalContent: ` -

You are currently editing the results of this stage.

-

If you switch to a later stage, your changes will be lost.

-

To keep them, please click Rerun to re-run the current stage.

`, - changeStepModalConfirmText: 'Switch Anyway', - changeStepModalCancelText: 'Cancel', - unlinkPipelineModalTitle: 'Unlink data pipeline', - unlinkPipelineModalContent: ` -

Once unlinked, this Dataset will no longer be connected to the current Data Pipeline.

-

Files that are already being parsed will continue until completion

-

Files that are not yet parsed will no longer be processed


-

Are you sure you want to proceed?

`, - unlinkPipelineModalConfirmText: 'Unlink', - }, - dataflow: { - parser: 'Parser', - parserDescription: - 'Extracts raw text and structure from files for downstream processing.', - tokenizer: 'Tokenizer', - tokenizerRequired: 'Please add the Tokenizer node first', - tokenizerDescription: - 'Transforms text into the required data structure (e.g., vector embeddings for Embedding Search) depending on the chosen search method.', - splitter: 'Token Splitter', - splitterDescription: - 'Split text into chunks by token length with optional delimiters and overlap.', - hierarchicalMergerDescription: - 'Split documents into sections by title hierarchy with regex rules for finer control.', - hierarchicalMerger: 'Title Splitter', - extractor: 'Context Generator', - extractorDescription: - 'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.', - outputFormat: 'Output format', - lang: 'Language', - fileFormats: 'File format', - fileFormatOptions: { - pdf: 'PDF', - spreadsheet: 'Spreadsheet', - image: 'Image', - email: 'Email', - 'text&markdown': 'Text & Markup', - word: 'Word', - slides: 'PPT', - audio: 'Audio', - }, - fields: 'Field', - addParser: 'Add Parser', - hierarchy: 'Hierarchy', - regularExpressions: 'Regular Expressions', - overlappedPercent: 'Overlapped percent', - searchMethod: 'Search method', - begin: 'File', - parserMethod: 'Parsing method', - systemPrompt: 'System Prompt', - systemPromptPlaceholder: - 'Enter system prompt for image analysis, if empty the system default value will be used', - exportJson: 'Export JSON', - viewResult: 'View Result', - running: 'Running', - summary: 'Augmented Context', - keywords: 'Keywords', - questions: 'Questions', - metadata: 'Metadata', - fieldName: 'Result Destination', - prompts: { - system: { - keywords: `Role -You are a text analyzer. - -Task -Extract the most important keywords/phrases of a given piece of text content. - -Requirements -- Summarize the text content, and give the top 5 important keywords/phrases. -- The keywords MUST be in the same language as the given piece of text content. -- The keywords are delimited by ENGLISH COMMA. -- Output keywords ONLY.`, - questions: `Role -You are a text analyzer. - -Task -Propose 3 questions about a given piece of text content. - -Requirements -- Understand and summarize the text content, and propose the top 3 important questions. -- The questions SHOULD NOT have overlapping meanings. -- The questions SHOULD cover the main content of the text as much as possible. -- The questions MUST be in the same language as the given piece of text content. -- One question per line. -- Output questions ONLY.`, - summary: `Act as a precise summarizer. Your task is to create a summary of the provided content that is both concise and faithful to the original. - -Key Instructions: -1. Accuracy: Strictly base the summary on the information given. Do not introduce any new facts, conclusions, or interpretations that are not explicitly stated. -2. Language: Write the summary in the same language as the source text. -3. Objectivity: Present the key points without bias, preserving the original intent and tone of the content. Do not editorialize. -4. Conciseness: Focus on the most important ideas, omitting minor details and fluff.`, - metadata: `Extract important structured information from the given content. Output ONLY a valid JSON string with no additional text. If no important structured information is found, output an empty JSON object: {}. - -Important structured information may include: names, dates, locations, events, key facts, numerical data, or other extractable entities.`, - }, - user: { - keywords: `Text Content -[Insert text here]`, - questions: `Text Content -[Insert text here]`, - summary: `Text to Summarize: -[Insert text here]`, - metadata: `Content: [INSERT CONTENT HERE]`, - }, - }, - cancel: 'Cancel', - switchPromptMessage: - 'The prompt word will change. Please confirm whether to abandon the existing prompt word?', - tokenizerSearchMethodOptions: { - full_text: 'Full-text', - embedding: 'Embedding', - }, - filenameEmbeddingWeight: 'Filename embedding weight', - tokenizerFieldsOptions: { - text: 'Processed Text', - keywords: 'Keywords', - questions: 'Questions', - summary: 'Augmented Context', - }, - imageParseMethodOptions: { - ocr: 'OCR', - }, - }, datasetOverview: { downloadTip: 'Files being downloaded from data sources. ', processingTip: 'Files being processed by data flows.', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index ed1058d..127d30d 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -65,6 +65,7 @@ export default { moreActions: '更多操作', disable: '禁用', enable: '启用', + onlyMe: '仅自己', team: '团队', public: '公开', unknown: '未知', @@ -1311,149 +1312,6 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 total: '总共 {{total}} 条', page: '{{page}}条/页', }, - dataflowParser: { - parseSummary: '解析摘要', - parseSummaryTip: '解析器: deepdoc', - rerunFromCurrentStep: '从当前步骤重新运行', - rerunFromCurrentStepTip: '已修改,点击重新运行。', - confirmRerun: '确认重新运行流程', - confirmRerunModalContent: ` -

- 您即将从 {{step}} 步骤开始重新运行该过程 -

-

这将:

-
    -
  • 从当前步骤开始覆盖现有结果
  • -
  • 创建新的日志条目进行跟踪
  • -
  • 之前的步骤将保持不变
  • -
`, - changeStepModalTitle: '切换步骤警告', - changeStepModalContent: ` -

您目前正在编辑此阶段的结果。

-

如果您切换到后续阶段,您的更改将会丢失。

-

要保留这些更改,请点击“重新运行”以重新运行当前阶段。

`, - changeStepModalConfirmText: '继续切换', - changeStepModalCancelText: '取消', - unlinkPipelineModalTitle: '解绑数据流', - unlinkPipelineModalContent: ` -

一旦取消链接,该数据集将不再连接到当前数据管道。

-

正在解析的文件将继续解析,直到完成。

-

尚未解析的文件将不再被处理。


-

你确定要继续吗?

`, - unlinkPipelineModalConfirmText: '解绑', - }, - dataflow: { - parser: '解析器', - parserDescription: '从文件中提取原始文本和结构以供下游处理。', - tokenizer: '分词器', - tokenizerRequired: '请先添加Tokenizer节点', - tokenizerDescription: - '根据所选的搜索方法,将文本转换为所需的数据结构(例如,用于嵌入搜索的向量嵌入)。', - splitter: '分词器拆分器', - splitterDescription: - '根据分词器长度将文本拆分成块,并带有可选的分隔符和重叠。', - hierarchicalMergerDescription: - '使用正则表达式规则按标题层次结构将文档拆分成多个部分,以实现更精细的控制。', - hierarchicalMerger: '标题拆分器', - extractor: '提取器', - extractorDescription: - '使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。', - outputFormat: '输出格式', - lang: '语言', - fileFormats: '文件格式', - fields: '字段', - addParser: '增加解析器', - hierarchy: '层次结构', - regularExpressions: '正则表达式', - overlappedPercent: '重叠百分比', - searchMethod: '搜索方法', - begin: '文件', - parserMethod: '解析方法', - systemPrompt: '系统提示词', - systemPromptPlaceholder: - '请输入用于图像分析的系统提示词,若为空则使用系统缺省值', - exportJson: '导出 JSON', - viewResult: '查看结果', - running: '运行中', - summary: '增强上下文', - keywords: '关键词', - questions: '问题', - metadata: '元数据', - fieldName: '结果目的地', - prompts: { - system: { - keywords: `角色 -你是一名文本分析员。 - -任务 -从给定的文本内容中提取最重要的关键词/短语。 - -要求 -- 总结文本内容,并给出最重要的5个关键词/短语。 -- 关键词必须与给定的文本内容使用相同的语言。 -- 关键词之间用英文逗号分隔。 -- 仅输出关键词。`, - questions: `角色 -你是一名文本分析员。 - -任务 -针对给定的文本内容提出3个问题。 - -要求 -- 理解并总结文本内容,并提出最重要的3个问题。 -- 问题的含义不应重叠。 -- 问题应尽可能涵盖文本的主要内容。 -- 问题必须与给定的文本内容使用相同的语言。 -- 每行一个问题。 -- 仅输出问题。`, - summary: `扮演一个精准的摘要者。你的任务是为提供的内容创建一个简洁且忠实于原文的摘要。 - -关键说明: -1. 准确性:摘要必须严格基于所提供的信息。请勿引入任何未明确说明的新事实、结论或解释。 -2. 语言:摘要必须使用与原文相同的语言。 -3. 客观性:不带偏见地呈现要点,保留内容的原始意图和语气。请勿进行编辑。 -4. 简洁性:专注于最重要的思想,省略细节和多余的内容。`, - metadata: `从给定内容中提取重要的结构化信息。仅输出有效的 JSON 字符串,不包含任何附加文本。如果未找到重要的结构化信息,则输出一个空的 JSON 对象:{}。 - -重要的结构化信息可能包括:姓名、日期、地点、事件、关键事实、数字数据或其他可提取实体。`, - }, - user: { - keywords: `文本内容 -[在此处插入文本]`, - questions: `文本内容 -[在此处插入文本]`, - summary: `要总结的文本: -[在此处插入文本]`, - metadata: `内容:[在此处插入内容]`, - }, - }, - cancel: '取消', - filenameEmbeddingWeight: '文件名嵌入权重', - switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?', - fileFormatOptions: { - pdf: 'PDF', - spreadsheet: '电子表格', - image: '图片', - email: '邮件', - 'text&markdown': '文本和标记', - word: 'Word', - slides: 'PPT', - audio: '音频', - }, - tokenizerSearchMethodOptions: { - full_text: '全文', - embedding: '嵌入', - }, - tokenizerFieldsOptions: { - text: '处理后的文本', - keywords: '关键词', - questions: '问题', - summary: '增强上下文', - }, - imageParseMethodOptions: { - ocr: 'OCR', - }, - }, datasetOverview: { downloadTip: '正在从数据源下载文件。', processingTip: '正在由数据流处理文件。', diff --git a/src/pages/agent/components/CreateAgentDialog.tsx b/src/pages/agent/components/CreateAgentDialog.tsx new file mode 100644 index 0000000..7664f21 --- /dev/null +++ b/src/pages/agent/components/CreateAgentDialog.tsx @@ -0,0 +1,260 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Box, + Typography, + List, + ListItemButton, + ListItemText, + Grid, + Card, + CardContent, + CardActions, + Avatar, +} from '@mui/material'; +import { Add as AddIcon } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; +import agentService from '@/services/agent_service'; +import type { IFlowTemplate } from '@/interfaces/database/agent'; +import { useSnackbar } from '@/hooks/useSnackbar'; +import { AgentCategory } from '@/constants/agent'; +import { IAgentCreateRequestBody } from '@/interfaces/request/agent'; +import { useAgentOperations } from '@/hooks/agent-hooks'; + +interface CreateAgentDialogProps { + open: boolean; + onClose: () => void; + onCreated?: (newId?: string) => void; +} + +const CreateAgentDialog: React.FC = ({ open, onClose, onCreated }) => { + const { t, i18n } = useTranslation(); + const { showMessage } = useSnackbar(); + const ops = useAgentOperations(); + const [loading, setLoading] = useState(false); + const [templates, setTemplates] = useState([]); + const [activeCategory, setActiveCategory] = useState('Recommended'); + const categoryRefs = useRef>({}); + const lang = i18n.language?.startsWith('zh') ? 'zh' : 'en'; + + useEffect(() => { + if (!open) return; + let mounted = true; + setLoading(true); + agentService + .listTemplates() + .then((res: any) => { + const data = res?.data?.data || []; + if (mounted) { + setTemplates(data); + } + }) + .catch((err: any) => { + console.error('listTemplates error', err); + showMessage.error(t('common.fetchFailed')); + }) + .finally(() => { + if (mounted) setLoading(false); + }); + return () => { + mounted = false; + }; + }, [open]); + + const categories = useMemo(() => { + const set = new Set(); + templates.forEach((tpl) => { + if (tpl.canvas_type) set.add(tpl.canvas_type); + }); + const arr = Array.from(set); + // 将 Recommended 放在第一位 + arr.sort((a, b) => { + if (a === 'Recommended') return -1; + if (b === 'Recommended') return 1; + return a.localeCompare(b); + }); + return arr; + }, [templates]); + + // 当分类列表变化时,默认选中 Recommended 或第一个分类 + useEffect(() => { + if (categories.length > 0) { + if (!categories.includes(activeCategory)) { + setActiveCategory(categories.includes('Recommended') ? 'Recommended' : categories[0]); + } + } + }, [categories]); + + // 保证选中项在可视区域 + useEffect(() => { + const el = categoryRefs.current[activeCategory]; + if (el) { + el.scrollIntoView({ block: 'nearest', inline: 'nearest' }); + } + }, [activeCategory]); + + const filteredTemplates = useMemo(() => { + return templates.filter((tpl) => tpl.canvas_type === activeCategory); + }, [templates, activeCategory]); + + const handleUseTemplate = async (tpl: IFlowTemplate) => { + try { + setLoading(true); + const body: IAgentCreateRequestBody = { + title: (tpl.title as any)?.[lang] || 'Untitled', + avatar: tpl.avatar, + description: (tpl.description as any)?.[lang] || '', + dsl: tpl.dsl, + canvas_category: (tpl as any).canvas_category || AgentCategory.AgentCanvas, + }; + const result = await ops.createAgent(body); + if (result.success) { + if (onCreated) onCreated(result.id); + } + onClose(); + } catch (err: any) { + console.error('create agent by template error', err); + } finally { + setLoading(false); + } + }; + + const handleCreateBlankAgent = async () => { + try { + setLoading(true); + const body: IAgentCreateRequestBody = { + title: lang === 'zh' ? '空白智能体' : 'Blank Agent', + dsl: { + components: {}, + history: [], + globals: {}, + retrieval: [], + graph: { nodes: [], edges: [] }, + messages: [], + }, + canvas_category: AgentCategory.AgentCanvas, + }; + const result = await ops.createAgent(body); + if (result.success) { + if (onCreated) onCreated(result.id); + } + onClose(); + } catch (err: any) { + console.error('create blank agent error', err); + } finally { + setLoading(false); + } + }; + + return ( + + {t('agent.createAgent')} + + + {/* 左侧分类 */} + + + {categories.map((cat) => ( + setActiveCategory(cat)} + ref={(el) => { categoryRefs.current[cat] = el; }} + > + + + ))} + + + + {/* 右侧模板网格 */} + + + {activeCategory === 'Recommended' && ( + + + + + + {lang === 'zh' ? '新建空白智能体' : 'New Blank Agent'} + + + + + )} + {filteredTemplates.map((tpl) => ( + + + + + + + + {(tpl.title as any)?.[lang] || (tpl.title as any)} + + + {(tpl.description as any)?.[lang] || ''} + + + + + + + + + + ))} + + {(!loading && filteredTemplates.length === 0) && ( + + {t('agent.noTemplates')} + + )} + + + + + + + + ); +}; + +export default CreateAgentDialog; \ No newline at end of file diff --git a/src/pages/agent/components/EditAgentDialog.tsx b/src/pages/agent/components/EditAgentDialog.tsx new file mode 100644 index 0000000..15a7e45 --- /dev/null +++ b/src/pages/agent/components/EditAgentDialog.tsx @@ -0,0 +1,223 @@ +import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Box, + TextField, + Avatar, + Grid, + IconButton, + FormControl, + InputLabel, + Select, + MenuItem, + Typography, +} from '@mui/material'; +import { PhotoCamera as PhotoCameraIcon, Delete as DeleteIcon } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; +import type { IFlow } from '@/interfaces/database/agent'; +import { useAgentOperations } from '@/hooks/agent-hooks'; + +export interface EditAgentDialogProps { + open: boolean; + agent: IFlow | null; + onClose: () => void; + onSaved?: () => void; +} + +const PERMISSION_OPTIONS = [ + { value: 'me', labelKey: 'common.onlyMe' }, + { value: 'team', labelKey: 'common.team' }, +] as const; + +const EditAgentDialog: React.FC = ({ open, agent, onClose, onSaved }) => { + const { t } = useTranslation(); + const ops = useAgentOperations(); + + const [title, setTitle] = useState(''); + const [avatar, setAvatar] = useState(''); + const [description, setDescription] = useState(''); + const [permission, setPermission] = useState<'me' | 'team' | 'public'>('me'); + const fileInputRef = useRef(null); + + const resizeImageToDataUrl = (file: File, maxDim = 256): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return reject(new Error('Canvas not supported')); + const scale = Math.min(1, maxDim / Math.max(img.width, img.height)); + const w = Math.max(1, Math.round(img.width * scale)); + const h = Math.max(1, Math.round(img.height * scale)); + canvas.width = w; + canvas.height = h; + ctx.drawImage(img, 0, 0, w, h); + // prefer jpeg for smaller size, fallback to png + const mime = file.type === 'image/png' ? 'image/png' : 'image/jpeg'; + const dataUrl = canvas.toDataURL(mime, 0.85); + resolve(dataUrl.replace(/\s/g, '')); + }; + img.onerror = () => reject(new Error('Invalid image')); + img.src = e.target?.result as string; + }; + reader.onerror = () => reject(new Error('Read failed')); + reader.readAsDataURL(file); + }); + }; + + const handleAvatarUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + try { + if (!file.type.startsWith('image/')) { + console.warn('Not an image file'); + return; + } + const dataUrl = await resizeImageToDataUrl(file, 256); + setAvatar(dataUrl); + } catch (err) { + console.error('Avatar upload error:', err); + } + }; + + const handleAvatarDelete = () => { + setAvatar(''); + }; + + const handleAvatarClick = () => { + fileInputRef.current?.click(); + }; + + useEffect(() => { + if (open && agent) { + setTitle(agent.title || ''); + setAvatar(agent.avatar || ''); + setDescription(agent.description || ''); + setPermission((agent.permission as any) || 'me'); + } + if (!open) { + // reset when closing to avoid stale state + setTitle(''); + setAvatar(''); + setDescription(''); + setPermission('me'); + } + }, [open, agent]); + + const canSubmit = useMemo(() => { + return Boolean(title && title.trim().length > 0 && agent?.id); + }, [title, agent]); + + const handleSave = async () => { + if (!agent?.id) return; + const res = await ops.editAgent({ + id: agent.id, + title: title.trim(), + avatar: avatar || undefined, + description: description?.trim() ? description.trim() : null, + permission, + canvas_category: agent.canvas_category as any, + } as any); + if (res.success) { + if (onSaved) onSaved(); + onClose(); + } + }; + + return ( + + {t('common.edit')} + + + + + + + {!avatar && } + + + + + {avatar && ( + + + + )} + + + + + + + + setTitle(e.target.value)} + fullWidth + required + /> + + setDescription(e.target.value)} + fullWidth + multiline + minRows={3} + /> + + + {t('agent.forms.permissionSettings')} + + + + + + + + + + ); +}; + +export default EditAgentDialog; \ No newline at end of file diff --git a/src/pages/agent/detail.tsx b/src/pages/agent/detail.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/agent/list.tsx b/src/pages/agent/list.tsx index 364a8f4..99e65b7 100644 --- a/src/pages/agent/list.tsx +++ b/src/pages/agent/list.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import { Box, Typography, @@ -9,12 +9,19 @@ import { Pagination, Stack, } from '@mui/material'; -import { Search as SearchIcon, Refresh as RefreshIcon } from '@mui/icons-material'; -import { useAgentList } from '@/hooks/agent-hooks'; +import { Search as SearchIcon, Refresh as RefreshIcon, Add as AddIcon } from '@mui/icons-material'; +import { useAgentList, useAgentOperations } from '@/hooks/agent-hooks'; import AgentGridView from '@/components/agent/AgentGridView'; +import CreateAgentDialog from '@/pages/agent/components/CreateAgentDialog'; +import EditAgentDialog from '@/pages/agent/components/EditAgentDialog'; +import { useTranslation } from 'react-i18next'; +import { useDialog } from '@/hooks/useDialog'; function AgentListPage() { const [searchValue, setSearchValue] = useState(''); + const [createOpen, setCreateOpen] = useState(false); + const [editOpen, setEditOpen] = useState(false); + const [editTarget, setEditTarget] = useState(null); const { agents, total, @@ -26,6 +33,10 @@ function AgentListPage() { refresh, } = useAgentList({ page: 1, page_size: 10 }); + const { t } = useTranslation(); + const dialog = useDialog(); + const ops = useAgentOperations(); + const totalPages = useMemo(() => { return Math.ceil((agents?.length || 0) / pageSize) || 1; }, [agents, pageSize]); @@ -36,20 +47,42 @@ function AgentListPage() { return (agents || []).slice(startIndex, endIndex); }, [agents, currentPage, pageSize]); - const handleSearch = useCallback(() => { - setKeywords(searchValue); - setCurrentPage(1); + const handleSearch = useCallback((value: string) => { + // 仅更新输入值,实际搜索在 500ms 防抖后触发 + setSearchValue(value); + }, []); + + // 500ms 防抖:在用户停止输入 500ms 后触发搜索 + useEffect(() => { + const handler = setTimeout(() => { + setKeywords(searchValue); + setCurrentPage(1); + }, 500); + return () => clearTimeout(handler); }, [searchValue, setKeywords, setCurrentPage]); return ( - Agent 列表 + {/* 页面标题 */} + + + {t('agent.agentList')} + + + setSearchValue(e.target.value)} + onChange={(e) => handleSearch(e.target.value)} placeholder="搜索名称或描述" size="small" InputProps={{ @@ -60,7 +93,6 @@ function AgentListPage() { ) }} /> - @@ -69,6 +101,19 @@ function AgentListPage() { agents={currentPageData} loading={loading} searchTerm={searchValue} + onCreateAgent={() => setCreateOpen(true)} + onEdit={(agent) => { setEditTarget(agent); setEditOpen(true); }} + onDelete={async (agent) => { + const confirmed = await dialog.confirm({ + title: t('dialog.confirmDelete'), + content: t('dialog.confirmDeleteMessage'), + confirmText: t('dialog.delete'), + cancelText: t('dialog.cancel'), + }); + if (!confirmed) return; + const res = await ops.deleteAgents([agent.id]); + if (res.success) refresh(); + }} /> {totalPages >= 1 && ( @@ -89,6 +134,20 @@ function AgentListPage() { )} + setCreateOpen(false)} + onCreated={() => { + setCreateOpen(false); + refresh(); + }} + /> + { setEditOpen(false); setEditTarget(null); }} + onSaved={() => { setEditOpen(false); setEditTarget(null); refresh(); }} + /> ); } diff --git a/src/services/agent_service.ts b/src/services/agent_service.ts index 9ede398..30bd377 100644 --- a/src/services/agent_service.ts +++ b/src/services/agent_service.ts @@ -1,6 +1,9 @@ import api from './api'; import request from '@/utils/request'; -import type { IAgentPaginationParams } from '@/interfaces/request/agent'; +import type { + IAgentCreateRequestBody, IAgentPaginationParams, + IAgentSetDSLRequestBody, IAgentSettingRequestBody +} from '@/interfaces/request/agent'; /** * 智能体服务 @@ -12,8 +15,67 @@ const agentService = { listCanvas: (params?: IAgentPaginationParams) => { return request.get(api.listTeamCanvas, { params }); }, + /** + * 获取系统模板列表 + */ + listTemplates: () => { + return request.get(api.listTemplates); + }, + /** + * 新建或更新 Canvas(Agent/Dataflow) + */ + setCanvas: (body: IAgentCreateRequestBody) => { + return request.post(api.setCanvas, body); + }, + /** + * 获取智能体详情 + * @param canvas_id Canvas ID + */ + getCanvas: (canvas_id: string) => { + return request.get(`${api.getCanvas}/${canvas_id}`); + }, + /** + * 获取智能体实时运行状态 + * @param canvas_id Canvas ID + */ + getCanvasSSE: (canvas_id: string) => { + return request.get(`${api.getCanvasSSE}/${canvas_id}`); + }, + /** + * 删除 Canvas(Agent/Dataflow) + * @param canvas_ids ID列表 + */ + removeCanvas: (canvas_ids: string[]) => { + return request.post(api.removeCanvas, { canvas_ids }); + }, + + /** + * 获取 Canvas 设置 + */ + settingAgent: (data: Partial) => { + return request.post(api.settingCanvas, data); + }, + /** + * 设置智能体DSL + */ + setAgentDSL: (data: Partial) => { + return request.post(api.setCanvas, data); + }, + /** + * 获取智能体版本详情 + * @param version_id 版本ID + */ + getVersion: (version_id: string) => { + return request.get(`${api.getVersion}/${version_id}`); + }, + /** + * 获取智能体版本列表 + * @param canvas_id Canvas ID + */ + getAgentVersionList: (canvas_id: string) => { + return request.get(`${api.getListVersion}/${canvas_id}`); + }, - }; export default agentService; \ No newline at end of file diff --git a/src/theme/index.ts b/src/theme/index.ts index 24b3a26..2a3427d 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -160,6 +160,54 @@ export const theme = createTheme({ }, }, }, + MuiTypography: { + styleOverrides: { + root: { + variants: [ + { + props: { className: 'ellipsis1' }, + style: { + lineClamp: 1, + WebkitLineClamp: 1, + WebkitBoxOrient: 'vertical', + display: '-webkit-box', + overflow: 'hidden', + }, + }, + { + props: { className: 'ellipsis2' }, + style: { + lineClamp: 2, + WebkitLineClamp: 2, + WebkitBoxOrient: 'vertical', + display: '-webkit-box', + overflow: 'hidden', + }, + }, + { + props: { className: 'ellipsis3' }, + style: { + lineClamp: 3, + WebkitLineClamp: 3, + WebkitBoxOrient: 'vertical', + display: '-webkit-box', + overflow: 'hidden', + }, + }, + { + props: { className: 'no-ellipsis' }, + style: { + lineClamp: 'unset', + WebkitLineClamp: 'unset', + WebkitBoxOrient: 'unset', + display: 'unset', + overflow: 'unset', + }, + }, + ], + }, + }, + }, }, }, xGridEnUS,