### What problem does this PR solve? Feat: Display agent history versions #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -33,6 +33,8 @@ export const enum AgentApiAction { | |||
| TestDbConnect = 'testDbConnect', | |||
| DebugSingle = 'debugSingle', | |||
| FetchInputForm = 'fetchInputForm', | |||
| FetchVersionList = 'fetchVersionList', | |||
| FetchVersion = 'fetchVersion', | |||
| } | |||
| export const EmptyDsl = { | |||
| @@ -396,3 +398,44 @@ export const useFetchInputForm = (componentId?: string) => { | |||
| return data; | |||
| }; | |||
| export const useFetchVersionList = () => { | |||
| const { id } = useParams(); | |||
| const { data, isFetching: loading } = useQuery< | |||
| Array<{ created_at: string; title: string; id: string }> | |||
| >({ | |||
| queryKey: [AgentApiAction.FetchVersionList], | |||
| initialData: [], | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| const { data } = await flowService.getListVersion({}, id); | |||
| return data?.data ?? []; | |||
| }, | |||
| }); | |||
| return { data, loading }; | |||
| }; | |||
| export const useFetchVersion = ( | |||
| version_id?: string, | |||
| ): { | |||
| data?: IFlow; | |||
| loading: boolean; | |||
| } => { | |||
| const { data, isFetching: loading } = useQuery({ | |||
| queryKey: [AgentApiAction.FetchVersion, version_id], | |||
| initialData: undefined, | |||
| gcTime: 0, | |||
| enabled: !!version_id, // Only call API when both values are provided | |||
| queryFn: async () => { | |||
| if (!version_id) return undefined; | |||
| const { data } = await flowService.getVersion({}, version_id); | |||
| return data?.data ?? undefined; | |||
| }, | |||
| }); | |||
| return { data, loading }; | |||
| }; | |||
| @@ -56,7 +56,7 @@ import { SwitchNode } from './node/switch-node'; | |||
| import { TemplateNode } from './node/template-node'; | |||
| import { ToolNode } from './node/tool-node'; | |||
| const nodeTypes: NodeTypes = { | |||
| export const nodeTypes: NodeTypes = { | |||
| ragNode: RagNode, | |||
| categorizeNode: CategorizeNode, | |||
| beginNode: BeginNode, | |||
| @@ -64,7 +64,7 @@ const ConditionBlock = ({ | |||
| function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { | |||
| const { positions } = useBuildSwitchHandlePositions({ data, id }); | |||
| return ( | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <ToolBar selected={selected} id={id} label={data.label} showRun={false}> | |||
| <NodeWrapper selected={selected}> | |||
| <CommonHandle | |||
| type="target" | |||
| @@ -38,6 +38,7 @@ import { | |||
| import { useShowEmbedModal } from './hooks/use-show-dialog'; | |||
| import { BeginQuery } from './interface'; | |||
| import { UploadAgentDialog } from './upload-agent-dialog'; | |||
| import { VersionDialog } from './version-dialog'; | |||
| function AgentDropdownMenuItem({ | |||
| children, | |||
| @@ -79,6 +80,11 @@ export default function Agent() { | |||
| handleRun(); | |||
| } | |||
| }, [getBeginNodeDataQuery, handleRun, showChatDrawer]); | |||
| const { | |||
| visible: versionDialogVisible, | |||
| hideModal: hideVersionDialog, | |||
| showModal: showVersionDialog, | |||
| } = useSetModalState(); | |||
| const { showEmbedModal, hideEmbedModal, embedVisible, beta } = | |||
| useShowEmbedModal(); | |||
| @@ -98,7 +104,7 @@ export default function Agent() { | |||
| <CirclePlay /> | |||
| Run app | |||
| </Button> | |||
| <Button variant={'secondary'}> | |||
| <Button variant={'secondary'} onClick={showVersionDialog}> | |||
| <History /> | |||
| History version | |||
| </Button> | |||
| @@ -159,6 +165,9 @@ export default function Agent() { | |||
| isAgent | |||
| ></EmbedDialog> | |||
| )} | |||
| {versionDialogVisible && ( | |||
| <VersionDialog hideModal={hideVersionDialog}></VersionDialog> | |||
| )} | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,131 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { | |||
| Dialog, | |||
| DialogContent, | |||
| DialogHeader, | |||
| DialogTitle, | |||
| } from '@/components/ui/dialog'; | |||
| import { Spin } from '@/components/ui/spin'; | |||
| import { | |||
| useFetchVersion, | |||
| useFetchVersionList, | |||
| } from '@/hooks/use-agent-request'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { downloadJsonFile } from '@/utils/file-util'; | |||
| import { | |||
| Background, | |||
| ConnectionMode, | |||
| ReactFlow, | |||
| ReactFlowProvider, | |||
| } from '@xyflow/react'; | |||
| import { ArrowDownToLine } from 'lucide-react'; | |||
| import { ReactNode, useCallback, useEffect, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { nodeTypes } from '../canvas'; | |||
| export function VersionDialog({ | |||
| hideModal, | |||
| }: IModalProps<any> & { initialName?: string; title?: ReactNode }) { | |||
| const { t } = useTranslation(); | |||
| const { data, loading } = useFetchVersionList(); | |||
| const [selectedId, setSelectedId] = useState<string>(''); | |||
| const { data: agent, loading: versionLoading } = useFetchVersion(selectedId); | |||
| const handleClick = useCallback( | |||
| (id: string) => () => { | |||
| setSelectedId(id); | |||
| }, | |||
| [], | |||
| ); | |||
| const downloadFile = useCallback(() => { | |||
| const graph = agent?.dsl.graph; | |||
| if (graph) { | |||
| downloadJsonFile(graph, agent?.title); | |||
| } | |||
| }, [agent?.dsl.graph, agent?.title]); | |||
| useEffect(() => { | |||
| if (data.length > 0) { | |||
| setSelectedId(data[0].id); | |||
| } | |||
| }, [data]); | |||
| return ( | |||
| <Dialog open onOpenChange={hideModal}> | |||
| <DialogContent className="max-w-[60vw]"> | |||
| <DialogHeader> | |||
| <DialogTitle>{t('flow.historyversion')}</DialogTitle> | |||
| </DialogHeader> | |||
| <section className="flex gap-2 relative"> | |||
| <div className="w-1/3 max-h-[60vh] overflow-auto min-h-[40vh]"> | |||
| {loading ? ( | |||
| <Spin className="top-1/2"></Spin> | |||
| ) : ( | |||
| <ul className="space-y-2"> | |||
| {data.map((x) => ( | |||
| <li | |||
| key={x.id} | |||
| className={cn('cursor-pointer', { | |||
| 'bg-card rounded p-1': x.id === selectedId, | |||
| })} | |||
| onClick={handleClick(x.id)} | |||
| > | |||
| {x.title} | |||
| </li> | |||
| ))} | |||
| </ul> | |||
| )} | |||
| </div> | |||
| <div className="relative flex-1 "> | |||
| {versionLoading ? ( | |||
| <Spin className="top-1/2" /> | |||
| ) : ( | |||
| <Card className="h-full"> | |||
| <CardContent className="h-full p-5"> | |||
| <section className="flex justify-between"> | |||
| <div> | |||
| <div className="pb-1">{agent?.title}</div> | |||
| <p className="text-text-sub-title text-xs"> | |||
| {formatDate(agent?.create_date)} | |||
| </p> | |||
| </div> | |||
| <Button variant={'ghost'} onClick={downloadFile}> | |||
| <ArrowDownToLine /> | |||
| </Button> | |||
| </section> | |||
| <ReactFlowProvider key={`flow-${selectedId}`}> | |||
| <ReactFlow | |||
| connectionMode={ConnectionMode.Loose} | |||
| nodes={agent?.dsl.graph?.nodes || []} | |||
| edges={ | |||
| agent?.dsl.graph?.edges.flatMap((x) => ({ | |||
| ...x, | |||
| type: 'default', | |||
| })) || [] | |||
| } | |||
| fitView | |||
| nodeTypes={nodeTypes} | |||
| edgeTypes={{}} | |||
| zoomOnScroll={true} | |||
| panOnDrag={true} | |||
| zoomOnDoubleClick={false} | |||
| preventScrolling={true} | |||
| minZoom={0.1} | |||
| > | |||
| <Background /> | |||
| </ReactFlow> | |||
| </ReactFlowProvider> | |||
| </CardContent> | |||
| </Card> | |||
| )} | |||
| </div> | |||
| </section> | |||
| </DialogContent> | |||
| </Dialog> | |||
| ); | |||
| } | |||
| @@ -199,6 +199,7 @@ | |||
| --background-checked: rgba(76, 164, 231, 1); | |||
| --background-highlight: rgba(76, 164, 231, 0.1); | |||
| --background-agent: rgba(22, 22, 24, 1); | |||
| --input-border: rgba(255, 255, 255, 0.2); | |||