diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index e695249..81814d4 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -12,7 +12,7 @@ const Sidebar = () => { const navItems = [ { text: t('header.knowledgeBase'), path: '/knowledge', icon: KnowledgeBasesIcon }, - { text: t('header.agent'), path: '/agent', icon: AgentIcon }, + { text: t('header.agent'), path: '/agents', icon: AgentIcon }, ]; return ( diff --git a/src/components/agent/AgentCard.tsx b/src/components/agent/AgentCard.tsx index 3c89d31..519ba81 100644 --- a/src/components/agent/AgentCard.tsx +++ b/src/components/agent/AgentCard.tsx @@ -50,7 +50,7 @@ const AgentCard: React.FC = ({ agent, onMenuClick, onViewAgent } const edgeCount = agent.dsl?.graph?.edges?.length ?? 0; return ( - + onViewAgent(agent)}> @@ -67,7 +67,10 @@ const AgentCard: React.FC = ({ agent, onMenuClick, onViewAgent } - onMenuClick(e, agent)}> + { + e.stopPropagation(); + onMenuClick(e, agent) + }}> @@ -85,7 +88,7 @@ const AgentCard: React.FC = ({ agent, onMenuClick, onViewAgent } = ({ handleMenuClose(); }; - const handleView = () => { - if (selectedAgent && onView) { - onView(selectedAgent); - } - handleMenuClose(); - }; - const handleViewAgent = (agent: IFlow) => { if (onView) { onView(agent); diff --git a/src/hooks/agent-hooks.ts b/src/hooks/agent-hooks.ts index 4f039be..c3ce55d 100644 --- a/src/hooks/agent-hooks.ts +++ b/src/hooks/agent-hooks.ts @@ -1,13 +1,13 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import agentService from '@/services/agent_service'; -import type { IFlow } from '@/interfaces/database/agent'; +import type { IAgent, IGraph } from '@/interfaces/database/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[]; + agents: IAgent[]; total: number; loading: boolean; error: string | null; @@ -32,7 +32,7 @@ export interface UseAgentListReturn extends UseAgentListState { * @param initialParams 初始参数 */ export const useAgentList = (initialParams?: IAgentPaginationParams) => { - const [agents, setAgents] = useState([]); + const [agents, setAgents] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -83,7 +83,6 @@ export const useAgentList = (initialParams?: IAgentPaginationParams) => { }; }; - export function useAgentOperations() { const { t } = useTranslation(); const { showMessage } = useSnackbar(); @@ -157,4 +156,139 @@ export function useAgentOperations() { const clearError = useCallback(() => setError(null), []); return { loading, error, createAgent, editAgent, deleteAgents, clearError }; +} + +/** + * 智能体详情钩子:拉取 Canvas 详情并解析出图(nodes/edges) + */ +export function useAgentDetail(id?: string) { + const [agent, setAgent] = useState(null); + const [graph, setGraph] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchDetail = useCallback(async () => { + if (!id) { + setError('Invalid agent id'); + return; + } + setLoading(true); + setError(null); + try { + const res = await agentService.getCanvas(id); + const body = res?.data ?? {}; + // 服务端常见返回结构:{ code, data: { id, title, dsl } } + const data = (body.data ?? body) as IAgent; + const dsl = (data as any)?.dsl ?? {}; + const g: IGraph | null = dsl?.graph; + setAgent(data); + setGraph(g); + logger.info('useAgentDetail fetched', { id, title: data?.title, nodes: g?.nodes?.length, edges: g?.edges?.length }); + } catch (err: any) { + const msg = err?.response?.data?.message || err?.message || 'Failed to fetch agent detail'; + setError(msg); + logger.error('useAgentDetail error', err); + } finally { + setLoading(false); + } + }, [id]); + + useEffect(() => { + fetchDetail(); + }, [fetchDetail]); + + return { agent, graph, loading, error, refresh: fetchDetail } as const; +}; + +export interface UseAgentCanvasOptions { + agent?: IAgent | null; + graph?: IGraph | null; + onReady?: (api: { fitView: () => void }) => void; + onAutoSaved?: (date: Date) => void; + disableNoteClick?: boolean; +} + +export function useAgentCanvas({ agent, graph, onReady, onAutoSaved, disableNoteClick = true }: UseAgentCanvasOptions) { + const [selectedNode, setSelectedNode] = useState(null); + const [hoverNode, setHoverNode] = useState(null); + const [sanitized, setSanitized] = useState(null); + const instRef = ({} as any) as { current?: any }; + + const sanitizeGraph = useCallback((g?: IGraph | null): IGraph | null => { + if (!g) return null; + const nodes = (g.nodes || []).map((n: any) => ({ ...n, draggable: false, selectable: true })); + const edges = (g.edges || []).map((e: any) => { + const { sourceHandle, targetHandle, ...rest } = e || {}; + return { ...rest }; + }); + return { nodes, edges }; + }, []); + + useEffect(() => { + setSanitized(sanitizeGraph(graph)); + }, [graph, sanitizeGraph]); + + const onInit = useCallback((inst: any) => { + try { + (instRef as any).current = inst; + onReady?.({ fitView: () => inst?.fitView?.({ padding: 0.2 }) }); + inst?.fitView?.({ padding: 0.2 }); + } catch (err) { + logger.error('useAgentCanvas onInit error', err); + } + }, [onReady]); + + const onNodeClick = useCallback((_: any, node: any) => { + if (disableNoteClick && node?.type === 'noteNode') return; + setSelectedNode(node); + }, [disableNoteClick]); + + const onPaneClick = useCallback(() => setSelectedNode(null), []); + const onNodeMouseEnter = useCallback((_: any, node: any) => setHoverNode(node), []); + + // 30秒自动保存(基于当前画布),确保在卸载与依赖变更时清理定时器 + const autoSaveTimerRef = useRef(null); + useEffect(() => { + // 清理已有定时器,避免重复 + if (autoSaveTimerRef.current) { + clearInterval(autoSaveTimerRef.current); + autoSaveTimerRef.current = null; + } + + // 数据不完整时不创建定时器(Canvas 不可用或退出) + if (!agent?.id || !agent?.title || !sanitized) { + return; + } + + autoSaveTimerRef.current = window.setInterval(async () => { + try { + const payload = { + id: agent.id, + title: agent.title, + dsl: { ...(agent.dsl || {}), graph: sanitized }, + }; + await agentService.setAgentDSL(payload as any); + onAutoSaved?.(new Date()); + } catch (err) { + logger.error('autoSave failed', err); + } + }, 30000); + + return () => { + if (autoSaveTimerRef.current) { + clearInterval(autoSaveTimerRef.current); + autoSaveTimerRef.current = null; + } + }; + }, [agent?.id, agent?.title, agent?.dsl, sanitized, onAutoSaved]); + + return { + sanitized, + selectedNode, + hoverNode, + onInit, + onNodeClick, + onPaneClick, + onNodeMouseEnter, + } as const; } \ No newline at end of file diff --git a/src/interfaces/database/agent.ts b/src/interfaces/database/agent.ts index ad5ecd5..a96c317 100644 --- a/src/interfaces/database/agent.ts +++ b/src/interfaces/database/agent.ts @@ -60,7 +60,7 @@ export interface IOperatorNode { params: Record; } -export declare interface IFlow { +export declare interface IAgent { avatar?: string; canvas_type: null; canvas_category?: string; @@ -78,7 +78,7 @@ export declare interface IFlow { operator_permission: number; } -export interface IFlowTemplate { +export interface IAgentTemplate { avatar: string; canvas_type: string; create_date: string; @@ -276,3 +276,24 @@ export interface IPipeLineListRequest { desc?: boolean; // canvas_category?: AgentCategory; } + +// { +// "create_date": "Thu, 06 Nov 2025 15:52:42 GMT", +// "create_time": 1762415562692, +// "id": "931d1cfabae511f097ca36b0b158b556", +// "title": "blank_2025_11_06_15_52_42", +// "update_date": "Thu, 06 Nov 2025 15:52:42 GMT", +// "update_time": 1762415562692, +// "user_canvas_id": "1edc3ddabadb11f08d0636b0b158b556" +// } + +export interface IAgentVersionItem { + id: string; + title: string; + create_date: string; + create_time: number; + update_date: string; + update_time: number; + user_canvas_id: string; +} + diff --git a/src/locales/en.ts b/src/locales/en.ts index 386307f..9ec42e6 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1238,6 +1238,10 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s createAgentSuccess: 'Agent created successfully', createAgentFailed: 'Agent creation failed', + // agent detail + lastAutoSavedAt: 'Last Auto Saved At {{date}}', + versions: 'Versions', + }, message: { registered: 'Registered!', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 127d30d..3122a40 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -1180,6 +1180,36 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 '如果你的模型供应商在这里没有列出,但是宣称 OpenAI-compatible,可以通过选择卡片 OpenAI-API-compatible 设置相关模型。', mcp: 'MCP', }, + agent: { + agentList: '智能体列表', + noMatchingAgents: '没有匹配的智能体', + noAgents: '没有智能体', + tryAdjustingFilters: '尝试调整筛选条件', + createFirstAgent: '创建你的第一个智能体', + createAgent: '创建智能体', + updatedAt: '更新时间', + creator: '创建者', + nodes: '节点', + edges: '边', + editAgent: '编辑智能体', + deleteAgent: '删除智能体', + + forms: { + title: '标题', + avatar: '头像', + description: '描述', + permissionSettings: '权限设置', + }, + + // create agent + useTemplate: '使用模板', + createAgentSuccess: '智能体创建成功', + createAgentFailed: '智能体创建失败', + + // agent detail + lastAutoSavedAt: '上次自动保存时间: {{date}}', + versions: '版本', + }, message: { registered: '注册成功', logout: '登出成功', diff --git a/src/pages/agent/canvas/NodeDrawer.tsx b/src/pages/agent/canvas/NodeDrawer.tsx new file mode 100644 index 0000000..7aff559 --- /dev/null +++ b/src/pages/agent/canvas/NodeDrawer.tsx @@ -0,0 +1,35 @@ +import { Drawer, Box, Typography, Divider } from '@mui/material'; + +export interface NodeDrawerProps { + open: boolean; + node: any | null; + onClose: () => void; +} + +export default function NodeDrawer({ open, node, onClose }: NodeDrawerProps) { + const label = node?.data?.label || ''; + const name = node?.data?.name || ''; + const type = node?.type || 'default'; + const form = node?.data?.form ?? {}; + + return ( + + + + {`${label}${name ? ' · ' + name : ''}`} + + + + 节点类型:{type} + + + + 表单数据 + + + {JSON.stringify(form, null, 2)} + + + + ); +} \ No newline at end of file diff --git a/src/pages/agent/canvas/index.tsx b/src/pages/agent/canvas/index.tsx new file mode 100644 index 0000000..5431418 --- /dev/null +++ b/src/pages/agent/canvas/index.tsx @@ -0,0 +1,89 @@ +import { Box } from "@mui/material"; +import { ReactFlow, Background, Controls, MiniMap, SmoothStepEdge } from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import { useMemo, useRef, useState, useCallback } from "react"; +import type { IGraph, IAgent } from '@/interfaces/database/agent'; +import NodeDrawer from './NodeDrawer'; +import ReadonlyNode from './node/ReadonlyNode'; +import NoteNode from './node/NoteNode'; +import { useAgentCanvas } from '@/hooks/agent-hooks'; + +const nodeTypes = { + beginNode: ReadonlyNode, + agentNode: ReadonlyNode, + messageNode: ReadonlyNode, + toolNode: ReadonlyNode, + noteNode: NoteNode, + retrievalNode: ReadonlyNode, + ragNode: ReadonlyNode, + switchNode: ReadonlyNode, + categorizeNode: ReadonlyNode, + iterationNode: ReadonlyNode, + iterationStartNode: ReadonlyNode, + keywordNode: ReadonlyNode, + relevantNode: ReadonlyNode, + logicNode: ReadonlyNode, + emailNode: ReadonlyNode, + tokenizerNode: ReadonlyNode, + templateNode: ReadonlyNode, + rewriteNode: ReadonlyNode, + invokeNode: ReadonlyNode, + parserNode: ReadonlyNode, + fileNode: ReadonlyNode, + extractorNode: ReadonlyNode, + splitterNode: ReadonlyNode, + placeholderNode: ReadonlyNode, + contextNode: ReadonlyNode, + group: ReadonlyNode, + generateNode: ReadonlyNode, +} as const; + +const edgeTypes = { + buttonEdge: SmoothStepEdge, + ButtonEdge: SmoothStepEdge, +} as const; + +export interface AgentCanvasProps { + agent?: IAgent | null; + graph?: IGraph | null; + onReady?: (api: { fitView: () => void }) => void; + onAutoSaved?: (date: Date) => void; +} + +function AgentCanvas({ agent, graph, onReady, onAutoSaved }: AgentCanvasProps) { + const { sanitized, selectedNode, hoverNode, onInit, + onNodeClick, onPaneClick, onNodeMouseEnter } = useAgentCanvas({ agent, graph, onReady, onAutoSaved }); + + return ( + + {sanitized && ( + + + + + + )} + + + ); +} + +export default AgentCanvas; \ No newline at end of file diff --git a/src/pages/agent/canvas/node/NoteNode.tsx b/src/pages/agent/canvas/node/NoteNode.tsx new file mode 100644 index 0000000..5c49a75 --- /dev/null +++ b/src/pages/agent/canvas/node/NoteNode.tsx @@ -0,0 +1,15 @@ +import { Box } from '@mui/material'; +import { Handle, Position, type NodeProps } from '@xyflow/react'; + +export default function NoteNode({ data }: NodeProps) { + const text = (data as any)?.form?.text || ''; + const name = (data as any)?.name || 'Note'; + return ( + + {name} + {String(text)} + + + + ); +} \ No newline at end of file diff --git a/src/pages/agent/canvas/node/ReadonlyNode.tsx b/src/pages/agent/canvas/node/ReadonlyNode.tsx new file mode 100644 index 0000000..aab6ea5 --- /dev/null +++ b/src/pages/agent/canvas/node/ReadonlyNode.tsx @@ -0,0 +1,15 @@ +import { Box } from '@mui/material'; +import { Handle, Position, type NodeProps } from '@xyflow/react'; + +export default function ReadonlyNode({ data }: NodeProps) { + const label = (data as any)?.label || ''; + const name = (data as any)?.name || ''; + return ( + + {label} + {name} + + + + ); +} \ No newline at end of file diff --git a/src/pages/agent/components/AgentTopbar.tsx b/src/pages/agent/components/AgentTopbar.tsx new file mode 100644 index 0000000..febb913 --- /dev/null +++ b/src/pages/agent/components/AgentTopbar.tsx @@ -0,0 +1,47 @@ +import { Breadcrumbs, Button, Box, Typography, IconButton } from '@mui/material'; +import SettingsIcon from '@mui/icons-material/Settings'; +import { useTranslation } from 'react-i18next'; +import { Link as RouterLink } from 'react-router-dom'; + +export interface AgentTopbarProps { + title?: string; + id?: string; + onRefresh?: () => void; + subtitle?: string; + actionsRight?: React.ReactNode; + onOpenVersions?: () => void; + onOpenSettings?: () => void; +} + +export default function AgentTopbar({ title, id, onRefresh, subtitle, actionsRight, onOpenVersions, onOpenSettings }: AgentTopbarProps) { + const { t } = useTranslation(); + return ( + + + + {t('header.agent')} + {title} + + {subtitle && ( + {subtitle} + )} + + + {actionsRight} + + + + + + + + ); +} \ No newline at end of file diff --git a/src/pages/agent/components/CreateAgentDialog.tsx b/src/pages/agent/components/CreateAgentDialog.tsx index 7664f21..6c71682 100644 --- a/src/pages/agent/components/CreateAgentDialog.tsx +++ b/src/pages/agent/components/CreateAgentDialog.tsx @@ -19,7 +19,7 @@ import { 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 type { IAgentTemplate } from '@/interfaces/database/agent'; import { useSnackbar } from '@/hooks/useSnackbar'; import { AgentCategory } from '@/constants/agent'; import { IAgentCreateRequestBody } from '@/interfaces/request/agent'; @@ -36,7 +36,7 @@ const CreateAgentDialog: React.FC = ({ open, onClose, on const { showMessage } = useSnackbar(); const ops = useAgentOperations(); const [loading, setLoading] = useState(false); - const [templates, setTemplates] = useState([]); + const [templates, setTemplates] = useState([]); const [activeCategory, setActiveCategory] = useState('Recommended'); const categoryRefs = useRef>({}); const lang = i18n.language?.startsWith('zh') ? 'zh' : 'en'; @@ -101,7 +101,7 @@ const CreateAgentDialog: React.FC = ({ open, onClose, on return templates.filter((tpl) => tpl.canvas_type === activeCategory); }, [templates, activeCategory]); - const handleUseTemplate = async (tpl: IFlowTemplate) => { + const handleUseTemplate = async (tpl: IAgentTemplate) => { try { setLoading(true); const body: IAgentCreateRequestBody = { diff --git a/src/pages/agent/components/EditAgentDialog.tsx b/src/pages/agent/components/EditAgentDialog.tsx index 15a7e45..50ccb09 100644 --- a/src/pages/agent/components/EditAgentDialog.tsx +++ b/src/pages/agent/components/EditAgentDialog.tsx @@ -18,12 +18,12 @@ import { } 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 type { IAgent } from '@/interfaces/database/agent'; import { useAgentOperations } from '@/hooks/agent-hooks'; export interface EditAgentDialogProps { open: boolean; - agent: IFlow | null; + agent?: IAgent; onClose: () => void; onSaved?: () => void; } diff --git a/src/pages/agent/components/VersionListDialog.tsx b/src/pages/agent/components/VersionListDialog.tsx new file mode 100644 index 0000000..4c39c3c --- /dev/null +++ b/src/pages/agent/components/VersionListDialog.tsx @@ -0,0 +1,144 @@ +import { useEffect, useMemo, useState, useCallback } from 'react'; +import { Dialog, DialogTitle, DialogContent, Box, List, ListItemButton, ListItemText, Divider, Typography } from '@mui/material'; +import { ReactFlow, Background, MiniMap, Controls, SmoothStepEdge } from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import agentService from '@/services/agent_service'; +import type { IAgentVersionItem, IAgent, IGraph } from '@/interfaces/database/agent'; +import ReadonlyNode from '@/pages/agent/canvas/node/ReadonlyNode'; +import NoteNode from '@/pages/agent/canvas/node/NoteNode'; + +function sanitizeGraph(g?: IGraph | null): IGraph | null { + if (!g) return null; + const nodes = (g.nodes || []).map((n: any) => ({ ...n, draggable: false, selectable: false })); + const edges = (g.edges || []).map((e: any) => { + const { sourceHandle, targetHandle, ...rest } = e || {}; + return { ...rest }; + }); + return { nodes, edges }; +} + +const nodeTypes = { + beginNode: ReadonlyNode, + agentNode: ReadonlyNode, + messageNode: ReadonlyNode, + toolNode: ReadonlyNode, + noteNode: NoteNode, + retrievalNode: ReadonlyNode, + ragNode: ReadonlyNode, + switchNode: ReadonlyNode, + categorizeNode: ReadonlyNode, + iterationNode: ReadonlyNode, + iterationStartNode: ReadonlyNode, + keywordNode: ReadonlyNode, + relevantNode: ReadonlyNode, + logicNode: ReadonlyNode, + emailNode: ReadonlyNode, + tokenizerNode: ReadonlyNode, + templateNode: ReadonlyNode, + rewriteNode: ReadonlyNode, + invokeNode: ReadonlyNode, + parserNode: ReadonlyNode, + fileNode: ReadonlyNode, + extractorNode: ReadonlyNode, + splitterNode: ReadonlyNode, + placeholderNode: ReadonlyNode, + contextNode: ReadonlyNode, + group: ReadonlyNode, + generateNode: ReadonlyNode, +} as const; + +const edgeTypes = { buttonEdge: SmoothStepEdge, ButtonEdge: SmoothStepEdge } as const; + +export interface VersionListDialogProps { + open: boolean; + canvasId?: string; + onClose: () => void; +} + +export default function VersionListDialog({ open, canvasId, onClose }: VersionListDialogProps) { + const [versions, setVersions] = useState([]); + const [selected, setSelected] = useState(null); + const [detail, setDetail] = useState(null); + + const graph = useMemo(() => sanitizeGraph(detail?.dsl?.graph), [detail]); + + const fetchList = useCallback(async () => { + if (!canvasId) return; + const res = await agentService.getAgentVersionList(canvasId); + const list = res?.data?.data || res?.data || []; + setVersions(list); + const first = list?.[0] || null; + setSelected(first); + }, [canvasId]); + + const fetchDetail = useCallback(async (versionId?: string) => { + if (!versionId) return; + const res = await agentService.getVersion(versionId); + const data = res?.data?.data || res?.data; + setDetail(data); + }, []); + + useEffect(() => { + if (open) fetchList(); + }, [open, fetchList]); + + useEffect(() => { + if (selected?.id) fetchDetail(selected.id); + }, [selected, fetchDetail]); + + return ( + + 历史版本 + + + + + {versions.map((v) => ( + setSelected(v)} + > + = 13 ? v.create_time : v.create_time * 1000)).toLocaleTimeString()}`} + /> + + ))} + + + + + + {detail?.title || selected?.title || '未选择版本'} + + + {graph && ( + + + + + + + + )} + + + + + ); +} \ No newline at end of file diff --git a/src/pages/agent/detail.tsx b/src/pages/agent/detail.tsx index e69de29..1f57abe 100644 --- a/src/pages/agent/detail.tsx +++ b/src/pages/agent/detail.tsx @@ -0,0 +1,65 @@ +import { Box, Typography, Alert, CircularProgress } from "@mui/material"; +import { useParams } from 'react-router-dom'; +import { useAgentDetail } from '@/hooks/agent-hooks'; +import { useState } from 'react'; +import AgentTopbar from '@/pages/agent/components/AgentTopbar'; +import AgentCanvas from '@/pages/agent/canvas'; +import VersionListDialog from '@/pages/agent/components/VersionListDialog'; +import EditAgentDialog from '@/pages/agent/components/EditAgentDialog'; +import dayjs from "dayjs"; +import { useTranslation } from "react-i18next"; + +function AgentDetailPage() { + const { id } = useParams(); + const { agent, graph, loading, error, refresh } = useAgentDetail(id); + const [canvasApi, setCanvasApi] = useState<{ fitView: () => void } | null>(null); + const [lastAutoSavedAt, setLastAutoSavedAt] = useState(null); + const [versionOpen, setVersionOpen] = useState(false); + const [editOpen, setEditOpen] = useState(false); + + const { t } = useTranslation(); + + const subtitle = (() => { + const ts = agent?.update_time ?? agent?.create_time; + if (!ts) return undefined; + const d = new Date(ts); + const dStr = dayjs(d).format('YYYY-MM-DD HH:mm:ss'); + return t('agent.lastAutoSavedAt', { date: dStr }); + })(); + + return ( + + setVersionOpen(true)} + onOpenSettings={() => setEditOpen(true)} + /> + + + {loading && ( + + + + )} + {error && ( + + {error} + + )} + setCanvasApi(api)} onAutoSaved={(date) => setLastAutoSavedAt(date)} /> + setVersionOpen(false)} /> + setEditOpen(false)} + onSaved={() => { setEditOpen(false); refresh(); }} + /> + + + ); +} + +export default AgentDetailPage; \ No newline at end of file diff --git a/src/pages/agent/list.tsx b/src/pages/agent/list.tsx index 99e65b7..d96defb 100644 --- a/src/pages/agent/list.tsx +++ b/src/pages/agent/list.tsx @@ -16,6 +16,7 @@ import CreateAgentDialog from '@/pages/agent/components/CreateAgentDialog'; import EditAgentDialog from '@/pages/agent/components/EditAgentDialog'; import { useTranslation } from 'react-i18next'; import { useDialog } from '@/hooks/useDialog'; +import { useNavigate } from 'react-router-dom'; function AgentListPage() { const [searchValue, setSearchValue] = useState(''); @@ -34,6 +35,7 @@ function AgentListPage() { } = useAgentList({ page: 1, page_size: 10 }); const { t } = useTranslation(); + const navigate = useNavigate(); const dialog = useDialog(); const ops = useAgentOperations(); @@ -103,6 +105,9 @@ function AgentListPage() { searchTerm={searchValue} onCreateAgent={() => setCreateOpen(true)} onEdit={(agent) => { setEditTarget(agent); setEditOpen(true); }} + onView={(agent) => { + navigate(`/agent/${agent.id}`); + }} onDelete={async (agent) => { const confirmed = await dialog.confirm({ title: t('dialog.confirmDelete'), diff --git a/src/routes/index.tsx b/src/routes/index.tsx index df85312..b8c5953 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -25,6 +25,7 @@ import MCP from '../pages/MCP'; import FormFieldTest from '../pages/FormFieldTest'; import ChunkParsedResult from '../pages/chunk/parsed-result'; import DocumentPreview from '../pages/chunk/document-preview'; +import AgentDetailPage from '@/pages/agent/detail'; const AppRoutes = () => { return ( @@ -45,10 +46,13 @@ const AppRoutes = () => { } /> - + } /> + + } /> + {/* 处理chunk相关路由 需要传入 kb_id doc_id */} } /> diff --git a/src/services/agent_service.ts b/src/services/agent_service.ts index 30bd377..e07abf5 100644 --- a/src/services/agent_service.ts +++ b/src/services/agent_service.ts @@ -61,6 +61,42 @@ const agentService = { setAgentDSL: (data: Partial) => { return request.post(api.setCanvas, data); }, + /** + * 运行 Canvas(同步接口) + */ + runCanvas: (body: Record) => { + return request.post(api.runCanvas, body); + }, + /** + * 数据库连接测试 + */ + testDbConnect: (body: Record) => { + return request.post(api.testDbConnect, body); + }, + /** + * 获取输入元素(Begin Inputs) + */ + getInputElements: (params?: Record) => { + return request.get(api.getInputElements, { params }); + }, + /** + * 调试运行(Debug 单步) + */ + debug: (body: Record) => { + return request.post(api.debug, body); + }, + /** + * 上传与 Canvas 相关的文件 + */ + uploadCanvasFile: (formData: FormData) => { + return request.post(api.uploadCanvasFile, formData); + }, + /** + * 获取 Canvas 运行追踪 + */ + trace: (params?: Record) => { + return request.get(api.trace, { params }); + }, /** * 获取智能体版本详情 * @param version_id 版本ID @@ -75,7 +111,13 @@ const agentService = { getAgentVersionList: (canvas_id: string) => { return request.get(`${api.getListVersion}/${canvas_id}`); }, - + /** + * 获取智能体 Prompts + * {Promise<{task_analysis: string,citation_guidelines: string, plan_generation: string, reflection: string}>} + */ + getCanvasPrompt: () => { + return request.get(api.prompts); + }, }; export default agentService; \ No newline at end of file diff --git a/src/services/api.ts b/src/services/api.ts index 370cb73..7aed1c7 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -177,7 +177,7 @@ export default { `${api_host}/canvas/${canvasId}/sessions`, fetchExternalAgentInputs: (canvasId: string) => `${ExternalApi}${api_host}/agentbots/${canvasId}/inputs`, - prompt: `${api_host}/canvas/prompts`, + prompts: `${api_host}/canvas/prompts`, cancelDataflow: (id: string) => `${api_host}/canvas/cancel/${id}`, downloadFile: `${api_host}/canvas/download`,