feat(agent): implement agent detail page with canvas view and version history

This commit is contained in:
2025-11-06 16:53:47 +08:00
parent 889d385709
commit e325beea4b
20 changed files with 672 additions and 26 deletions

View File

@@ -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<IFlow[]>([]);
const [agents, setAgents] = useState<IAgent[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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<IAgent | null>(null);
const [graph, setGraph] = useState<IGraph | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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<any | null>(null);
const [hoverNode, setHoverNode] = useState<any | null>(null);
const [sanitized, setSanitized] = useState<IGraph | null>(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<number | null>(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;
}