mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
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  _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:
@ -90,6 +90,53 @@ export const useFetchFlowList = (): { data: IFlow[]; loading: boolean } => {
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const useFetchListVersion = (
|
||||
canvas_id: string,
|
||||
): {
|
||||
data: {
|
||||
created_at: string;
|
||||
title: string;
|
||||
id: string;
|
||||
}[];
|
||||
loading: boolean;
|
||||
} => {
|
||||
const { data, isFetching: loading } = useQuery({
|
||||
queryKey: ['fetchListVersion'],
|
||||
initialData: [],
|
||||
gcTime: 0,
|
||||
queryFn: async () => {
|
||||
const { data } = await flowService.getListVersion({}, canvas_id);
|
||||
|
||||
return data?.data ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const useFetchVersion = (
|
||||
version_id?: string,
|
||||
): {
|
||||
data?: IFlow;
|
||||
loading: boolean;
|
||||
} => {
|
||||
const { data, isFetching: loading } = useQuery({
|
||||
queryKey: ['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 };
|
||||
};
|
||||
|
||||
export const useFetchFlow = (): {
|
||||
data: IFlow;
|
||||
loading: boolean;
|
||||
|
||||
@ -1194,6 +1194,16 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
nextStep: 'Next step',
|
||||
datatype: 'MINE type of the HTTP request',
|
||||
insertVariableTip: `Enter / Insert variables`,
|
||||
historyversion: 'History version',
|
||||
filename: 'File name',
|
||||
version: {
|
||||
created: 'Created',
|
||||
details: 'Version details',
|
||||
dsl: 'DSL',
|
||||
download: 'Download',
|
||||
version: 'Version',
|
||||
select: 'No version selected',
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
profile: 'All rights reserved @ React',
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
184
web/src/pages/flow/history-version-modal/index.tsx
Normal file
184
web/src/pages/flow/history-version-modal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -6,6 +6,8 @@ const {
|
||||
getCanvas,
|
||||
getCanvasSSE,
|
||||
setCanvas,
|
||||
getListVersion,
|
||||
getVersion,
|
||||
listCanvas,
|
||||
resetCanvas,
|
||||
removeCanvas,
|
||||
@ -29,6 +31,14 @@ const methods = {
|
||||
url: setCanvas,
|
||||
method: 'post',
|
||||
},
|
||||
getListVersion: {
|
||||
url: getListVersion,
|
||||
method: 'get',
|
||||
},
|
||||
getVersion: {
|
||||
url: getVersion,
|
||||
method: 'get',
|
||||
},
|
||||
listCanvas: {
|
||||
url: listCanvas,
|
||||
method: 'get',
|
||||
@ -63,6 +73,6 @@ const methods = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const chatService = registerServer<keyof typeof methods>(methods, request);
|
||||
const flowService = registerServer<keyof typeof methods>(methods, request);
|
||||
|
||||
export default chatService;
|
||||
export default flowService;
|
||||
|
||||
@ -127,6 +127,8 @@ export default {
|
||||
getCanvasSSE: `${api_host}/canvas/getsse`,
|
||||
removeCanvas: `${api_host}/canvas/rm`,
|
||||
setCanvas: `${api_host}/canvas/set`,
|
||||
getListVersion: `${api_host}/canvas/getlistversion`,
|
||||
getVersion: `${api_host}/canvas/getversion`,
|
||||
resetCanvas: `${api_host}/canvas/reset`,
|
||||
runCanvas: `${api_host}/canvas/completion`,
|
||||
testDbConnect: `${api_host}/canvas/test_db_connect`,
|
||||
|
||||
Reference in New Issue
Block a user