diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py index 8dcbb254c..0f01dd26f 100644 --- a/api/apps/canvas_app.py +++ b/api/apps/canvas_app.py @@ -18,13 +18,14 @@ import traceback from flask import request, Response from flask_login import login_required, current_user from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService +from api.db.services.user_canvas_version import UserCanvasVersionService from api.settings import RetCode from api.utils import get_uuid from api.utils.api_utils import get_json_result, server_error_response, validate_request, get_data_error_result from agent.canvas import Canvas from peewee import MySQLDatabase, PostgresqlDatabase from api.db.db_models import APIToken - +import time @manager.route('/templates', methods=['GET']) # noqa: F821 @login_required @@ -61,7 +62,6 @@ def save(): req["user_id"] = current_user.id if not isinstance(req["dsl"], str): req["dsl"] = json.dumps(req["dsl"], ensure_ascii=False) - req["dsl"] = json.loads(req["dsl"]) if "id" not in req: if UserCanvasService.query(user_id=current_user.id, title=req["title"].strip()): @@ -75,8 +75,13 @@ def save(): data=False, message='Only owner of canvas authorized for this operation.', code=RetCode.OPERATING_ERROR) UserCanvasService.update_by_id(req["id"], req) + # save version + UserCanvasVersionService.insert( user_canvas_id=req["id"], dsl=req["dsl"], title="{0}_{1}".format(req["title"], time.strftime("%Y_%m_%d_%H_%M_%S"))) + UserCanvasVersionService.delete_all_versions(req["id"]) return get_json_result(data=req) + + @manager.route('/get/', methods=['GET']) # noqa: F821 @login_required @@ -284,3 +289,27 @@ def test_db_connect(): except Exception as e: return server_error_response(e) + + + +#api get list version dsl of canvas +@manager.route('/getlistversion/', methods=['GET']) # noqa: F821 +@login_required +def getlistversion(canvas_id): + try: + list =sorted([c.to_dict() for c in UserCanvasVersionService.list_by_canvas_id(canvas_id)], key=lambda x: x["update_time"]*-1) + return get_json_result(data=list) + except Exception as e: + return get_data_error_result(message=f"Error getting history files: {e}") + +#api get version dsl of canvas +@manager.route('/getversion/', methods=['GET']) # noqa: F821 +@login_required +def getversion( version_id): + try: + + e, version = UserCanvasVersionService.get_by_id(version_id) + if version: + return get_json_result(data=version.to_dict()) + except Exception as e: + return get_json_result(data=f"Error getting history file: {e}") diff --git a/api/db/db_models.py b/api/db/db_models.py index 06d5aa96a..57437f35c 100644 --- a/api/db/db_models.py +++ b/api/db/db_models.py @@ -988,6 +988,16 @@ class CanvasTemplate(DataBaseModel): class Meta: db_table = "canvas_template" +class UserCanvasVersion(DataBaseModel): + id = CharField(max_length=32, primary_key=True) + user_canvas_id = CharField(max_length=255, null=False, help_text="user_canvas_id", index=True) + + title = CharField(max_length=255, null=True, help_text="Canvas title") + description = TextField(null=True, help_text="Canvas description") + dsl = JSONField(null=True, default={}) + + class Meta: + db_table = "user_canvas_version" def migrate_db(): with DB.transaction(): diff --git a/api/db/services/user_canvas_version.py b/api/db/services/user_canvas_version.py new file mode 100644 index 000000000..414a1a8e1 --- /dev/null +++ b/api/db/services/user_canvas_version.py @@ -0,0 +1,43 @@ +from api.db.db_models import UserCanvasVersion, DB +from api.db.services.common_service import CommonService +from peewee import DoesNotExist + +class UserCanvasVersionService(CommonService): + model = UserCanvasVersion + + + @classmethod + @DB.connection_context() + def list_by_canvas_id(cls, user_canvas_id): + try: + user_canvas_version = cls.model.select( + *[cls.model.id, + cls.model.create_time, + cls.model.title, + cls.model.create_date, + cls.model.update_date, + cls.model.user_canvas_id, + cls.model.update_time] + ).where(cls.model.user_canvas_id == user_canvas_id) + return user_canvas_version + except DoesNotExist: + return None + except Exception: + return None + + @classmethod + @DB.connection_context() + def delete_all_versions(cls, user_canvas_id): + try: + user_canvas_version = cls.model.select().where(cls.model.user_canvas_id == user_canvas_id).order_by(cls.model.create_time.desc()) + if user_canvas_version.count() > 20: + for i in range(20, user_canvas_version.count()): + cls.delete(user_canvas_version[i].id) + return True + except DoesNotExist: + return None + except Exception: + return None + + + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 676f167d6..fad404c82 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -17,6 +17,8 @@ services: - ./nginx/ragflow.conf:/etc/nginx/conf.d/ragflow.conf - ./nginx/proxy.conf:/etc/nginx/proxy.conf - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - ../history_data_agent:/ragflow/history_data_agent + env_file: .env environment: - TZ=${TIMEZONE} diff --git a/web/src/hooks/flow-hooks.ts b/web/src/hooks/flow-hooks.ts index 07905e1ad..55de6e743 100644 --- a/web/src/hooks/flow-hooks.ts +++ b/web/src/hooks/flow-hooks.ts @@ -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; diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 477166e5e..080e60c03 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -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', diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx index 6ded286b1..e9ce10ac7 100644 --- a/web/src/pages/flow/canvas/index.tsx +++ b/web/src/pages/flow/canvas/index.tsx @@ -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, }; diff --git a/web/src/pages/flow/header/index.tsx b/web/src/pages/flow/header/index.tsx index 161c92a95..1ded880d0 100644 --- a/web/src/pages/flow/header/index.tsx +++ b/web/src/pages/flow/header/index.tsx @@ -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 ( <> { > {t('embedIntoSite', { keyPrefix: 'common' })} + {embedVisible && ( @@ -95,6 +106,13 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { isAgent > )} + {visibleHistoryVersionModal && ( + setVisibleHistoryVersionModal(false)} + > + )} ); }; diff --git a/web/src/pages/flow/history-version-modal/index.tsx b/web/src/pages/flow/history-version-modal/index.tsx new file mode 100644 index 000000000..96213c694 --- /dev/null +++ b/web/src/pages/flow/history-version-modal/index.tsx @@ -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(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 ( + + document.body} + > + + + {loading && } + {!loading && data.length === 0 && ( + + )} + {!loading && data.length > 0 && ( + ( + { + e.stopPropagation(); + setSelectedVersion(item); + }} + style={{ + cursor: 'pointer', + background: + selectedVersion?.id === item.id ? '#f0f5ff' : 'inherit', + padding: '8px 12px', + borderRadius: '4px', + }} + > + + + )} + /> + )} + + + {/* Right panel - Version details */} + + {selectedVersion ? ( + + + {/* Add actions for the selected version (restore, download, etc.) */} + +
+ + {t('version.download')} + +
+ +
+ + {selectedVersion.title || '-'} + + + + {t('version.created')}: {selectedVersion.create_date} + + + {/*render dsl form api*/} + {loadingVersion && } + {!loadingVersion && flow?.dsl && ( + +
+ ({ + ...x, + type: 'default', + })) || [] + } + fitView + nodeTypes={nodeTypes} + edgeTypes={{}} + zoomOnScroll={true} + panOnDrag={true} + zoomOnDoubleClick={false} + preventScrolling={true} + minZoom={0.1} + > + + +
+
+ )} +
+ ) : ( + + )} + +
+
+
+ ); +} diff --git a/web/src/services/flow-service.ts b/web/src/services/flow-service.ts index 87d26ffce..f9619d747 100644 --- a/web/src/services/flow-service.ts +++ b/web/src/services/flow-service.ts @@ -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(methods, request); +const flowService = registerServer(methods, request); -export default chatService; +export default flowService; diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 61f4343c2..a6fae88a9 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -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`,