Feat: support agent version history. (#6130)

### What problem does this PR solve?
Add history version save
- Allows users to view and download agent files by version revision
history

![image](https://github.com/user-attachments/assets/c300375d-8b97-4230-9fc4-83d148137132)

_Briefly describe what this PR aims to solve. Include background context
that will help reviewers understand the purpose of the PR._

### Type of change

- [ ] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
This commit is contained in:
so95
2025-03-19 14:22:53 +07:00
committed by GitHub
parent e689532e6e
commit 53ac27c3ff
11 changed files with 362 additions and 7 deletions

View File

@ -46,7 +46,7 @@ import { RewriteNode } from './node/rewrite-node';
import { SwitchNode } from './node/switch-node';
import { TemplateNode } from './node/template-node';
const nodeTypes: NodeTypes = {
export const nodeTypes: NodeTypes = {
ragNode: RagNode,
categorizeNode: CategorizeNode,
beginNode: BeginNode,
@ -66,7 +66,7 @@ const nodeTypes: NodeTypes = {
iterationStartNode: IterationStartNode,
};
const edgeTypes = {
export const edgeTypes = {
buttonEdge: ButtonEdge,
};

View File

@ -18,6 +18,10 @@ import {
} from '../hooks/use-save-graph';
import { BeginQuery } from '../interface';
import {
HistoryVersionModal,
useHistoryVersionModal,
} from '../history-version-modal';
import styles from './index.less';
interface IProps {
@ -36,7 +40,8 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal();
const isBeginNodeDataQuerySafe = useGetBeginNodeDataQueryIsSafe();
const { setVisibleHistoryVersionModal, visibleHistoryVersionModal } =
useHistoryVersionModal();
const handleShowEmbedModal = useCallback(() => {
showEmbedModal();
}, [showEmbedModal]);
@ -50,6 +55,9 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
}
}, [getBeginNodeDataQuery, handleRun, showChatDrawer]);
const showListVersion = useCallback(() => {
setVisibleHistoryVersionModal(true);
}, [setVisibleHistoryVersionModal]);
return (
<>
<Flex
@ -83,6 +91,9 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
>
<b>{t('embedIntoSite', { keyPrefix: 'common' })}</b>
</Button>
<Button type="primary" onClick={showListVersion}>
<b>{t('historyversion')}</b>
</Button>
</Space>
</Flex>
{embedVisible && (
@ -95,6 +106,13 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
isAgent
></EmbedModal>
)}
{visibleHistoryVersionModal && (
<HistoryVersionModal
id={id || ''}
visible={visibleHistoryVersionModal}
hideModal={() => setVisibleHistoryVersionModal(false)}
></HistoryVersionModal>
)}
</>
);
};

View File

@ -0,0 +1,184 @@
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchListVersion, useFetchVersion } from '@/hooks/flow-hooks';
import {
Background,
ConnectionMode,
ReactFlow,
ReactFlowProvider,
} from '@xyflow/react';
import { Card, Col, Empty, List, Modal, Row, Spin, Typography } from 'antd';
import React, { useState } from 'react';
import { nodeTypes } from '../canvas';
export function useHistoryVersionModal() {
const [visibleHistoryVersionModal, setVisibleHistoryVersionModal] =
React.useState(false);
return {
visibleHistoryVersionModal,
setVisibleHistoryVersionModal,
};
}
type HistoryVersionModalProps = {
visible: boolean;
hideModal: () => void;
id: string;
};
export function HistoryVersionModal({
visible,
hideModal,
id,
}: HistoryVersionModalProps) {
const { t } = useTranslate('flow');
const { data, loading } = useFetchListVersion(id);
const [selectedVersion, setSelectedVersion] = useState<any>(null);
const { data: flow, loading: loadingVersion } = useFetchVersion(
selectedVersion?.id,
);
React.useEffect(() => {
if (!loading && data?.length > 0 && !selectedVersion) {
setSelectedVersion(data[0]);
}
}, [data, loading, selectedVersion]);
const downloadfile = React.useCallback(
function (e: any) {
e.stopPropagation();
console.log('Restore version:', selectedVersion);
// Create a JSON blob and trigger download
const jsonContent = JSON.stringify(flow?.dsl.graph, null, 2);
const blob = new Blob([jsonContent], {
type: 'application/json',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedVersion.filename || 'flow-version'}-${selectedVersion.id}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
[selectedVersion, flow?.dsl],
);
return (
<React.Fragment>
<Modal
title={t('historyversion')}
open={visible}
width={'80vw'}
onCancel={hideModal}
footer={null}
getContainer={() => document.body}
>
<Row gutter={16} style={{ height: '60vh' }}>
<Col span={10} style={{ height: '100%', overflowY: 'auto' }}>
{loading && <Spin />}
{!loading && data.length === 0 && (
<Empty description="No versions found" />
)}
{!loading && data.length > 0 && (
<List
itemLayout="horizontal"
dataSource={data}
pagination={{
pageSize: 5,
simple: true,
}}
renderItem={(item) => (
<List.Item
key={item.id}
onClick={(e) => {
e.stopPropagation();
setSelectedVersion(item);
}}
style={{
cursor: 'pointer',
background:
selectedVersion?.id === item.id ? '#f0f5ff' : 'inherit',
padding: '8px 12px',
borderRadius: '4px',
}}
>
<List.Item.Meta
title={`${t('filename')}: ${item.title || '-'}`}
description={item.created_at}
/>
</List.Item>
)}
/>
)}
</Col>
{/* Right panel - Version details */}
<Col span={14} style={{ height: '100%', overflowY: 'auto' }}>
{selectedVersion ? (
<Card title={t('version.details')} bordered={false}>
<Row gutter={[16, 16]}>
{/* Add actions for the selected version (restore, download, etc.) */}
<Col span={24}>
<div style={{ textAlign: 'right' }}>
<Typography.Link onClick={downloadfile}>
{t('version.download')}
</Typography.Link>
</div>
</Col>
</Row>
<Typography.Title level={4}>
{selectedVersion.title || '-'}
</Typography.Title>
<Typography.Text
type="secondary"
style={{ display: 'block', marginBottom: 16 }}
>
{t('version.created')}: {selectedVersion.create_date}
</Typography.Text>
{/*render dsl form api*/}
{loadingVersion && <Spin />}
{!loadingVersion && flow?.dsl && (
<ReactFlowProvider key={`flow-${selectedVersion.id}`}>
<div
style={{
height: '400px',
position: 'relative',
zIndex: 0,
}}
>
<ReactFlow
connectionMode={ConnectionMode.Loose}
nodes={flow?.dsl.graph?.nodes || []}
edges={
flow?.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>
</div>
</ReactFlowProvider>
)}
</Card>
) : (
<Empty description={t('version.select')} />
)}
</Col>
</Row>
</Modal>
</React.Fragment>
);
}