refactor: snapshot store to diff (#3155)

* refactor: snapshot store to diff

* change initial state position

* fix old snapshot format

* encapsulate json diff
This commit is contained in:
heheer
2024-11-21 13:12:42 +08:00
committed by GitHub
parent 4f55025906
commit 9b2c3b242a
11 changed files with 364 additions and 101 deletions

121
pnpm-lock.yaml generated
View File

@@ -22,7 +22,7 @@ importers:
version: 13.3.0
next-i18next:
specifier: 15.3.0
version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
prettier:
specifier: 3.2.4
version: 3.2.4
@@ -61,7 +61,7 @@ importers:
version: 4.0.2
next:
specifier: 14.2.5
version: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
openai:
specifier: 4.61.0
version: 4.61.0(encoding@0.1.13)
@@ -201,7 +201,7 @@ importers:
version: 1.4.5-lts.1
next:
specifier: 14.2.5
version: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
nextjs-cors:
specifier: ^2.2.0
version: 2.2.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))
@@ -277,7 +277,7 @@ importers:
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@chakra-ui/next-js':
specifier: 2.1.5
version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)
version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)
'@chakra-ui/react':
specifier: 2.8.1
version: 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -340,7 +340,7 @@ importers:
version: 4.17.21
next-i18next:
specifier: 15.3.0
version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
papaparse:
specifier: ^5.4.1
version: 5.4.1
@@ -486,6 +486,9 @@ importers:
json5:
specifier: ^2.2.3
version: 2.2.3
jsondiffpatch:
specifier: ^0.6.0
version: 0.6.0
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
@@ -700,7 +703,7 @@ importers:
version: 6.3.4
ts-jest:
specifier: ^29.1.0
version: 29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0))(typescript@5.5.3)
version: 29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3)
ts-loader:
specifier: ^9.4.3
version: 9.5.1(typescript@5.5.3)(webpack@5.92.1)
@@ -3177,8 +3180,8 @@ packages:
'@tanstack/react-query@4.36.1':
resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react: 18.3.1
react-dom: 18.3.1
react-native: '*'
peerDependenciesMeta:
react-dom:
@@ -3331,6 +3334,9 @@ packages:
'@types/decompress@4.2.7':
resolution: {integrity: sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g==}
'@types/diff-match-patch@1.0.36':
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
'@types/eslint-scope@3.7.7':
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
@@ -4848,6 +4854,9 @@ packages:
dezalgo@1.0.4:
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
diff-match-patch@1.0.5:
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
diff-sequences@29.6.3:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -5139,6 +5148,7 @@ packages:
eslint@8.56.0:
resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
espree@9.6.1:
@@ -6308,6 +6318,11 @@ packages:
jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsondiffpatch@0.6.0:
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
@@ -10462,6 +10477,14 @@ snapshots:
next: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
react: 18.3.1
'@chakra-ui/next-js@2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)':
dependencies:
'@chakra-ui/react': 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@emotion/cache': 11.11.0
'@emotion/react': 11.11.1(@types/react@18.3.1)(react@18.3.1)
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
react: 18.3.1
'@chakra-ui/number-input@2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies:
'@chakra-ui/counter': 2.1.0(react@18.3.1)
@@ -12393,6 +12416,8 @@ snapshots:
dependencies:
'@types/node': 22.7.8
'@types/diff-match-patch@1.0.36': {}
'@types/eslint-scope@3.7.7':
dependencies:
'@types/eslint': 8.56.10
@@ -13203,7 +13228,7 @@ snapshots:
axios@1.7.7:
dependencies:
follow-redirects: 1.15.9(debug@4.3.7)
follow-redirects: 1.15.9
form-data: 4.0.1
proxy-from-env: 1.1.0
transitivePeerDependencies:
@@ -14182,6 +14207,8 @@ snapshots:
asap: 2.0.6
wrappy: 1.0.2
diff-match-patch@1.0.5: {}
diff-sequences@29.6.3: {}
diff@4.0.2: {}
@@ -14982,6 +15009,8 @@ snapshots:
follow-redirects@1.15.6: {}
follow-redirects@1.15.9: {}
follow-redirects@1.15.9(debug@4.3.4):
optionalDependencies:
debug: 4.3.4
@@ -15044,7 +15073,7 @@ snapshots:
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tslib: 2.7.0
tslib: 2.8.0
optionalDependencies:
'@emotion/is-prop-valid': 0.8.8
@@ -16141,6 +16170,12 @@ snapshots:
jsonc-parser@3.3.1: {}
jsondiffpatch@0.6.0:
dependencies:
'@types/diff-match-patch': 1.0.36
chalk: 5.3.0
diff-match-patch: 1.0.5
jsonfile@6.1.0:
dependencies:
universalify: 2.0.1
@@ -17323,6 +17358,18 @@ snapshots:
react: 18.3.1
react-i18next: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-i18next@15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.24.8
'@types/hoist-non-react-statics': 3.3.5
core-js: 3.37.1
hoist-non-react-statics: 3.3.2
i18next: 23.11.5
i18next-fs-backend: 2.3.1
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
react: 18.3.1
react-i18next: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8):
dependencies:
'@next/env': 14.2.5
@@ -17349,10 +17396,36 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8):
dependencies:
'@next/env': 14.2.5
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001669
graceful-fs: 4.2.11
postcss: 8.4.31
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
styled-jsx: 5.1.1(react@18.3.1)
optionalDependencies:
'@next/swc-darwin-arm64': 14.2.5
'@next/swc-darwin-x64': 14.2.5
'@next/swc-linux-arm64-gnu': 14.2.5
'@next/swc-linux-arm64-musl': 14.2.5
'@next/swc-linux-x64-gnu': 14.2.5
'@next/swc-linux-x64-musl': 14.2.5
'@next/swc-win32-arm64-msvc': 14.2.5
'@next/swc-win32-ia32-msvc': 14.2.5
'@next/swc-win32-x64-msvc': 14.2.5
sass: 1.77.8
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
nextjs-cors@2.2.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)):
dependencies:
cors: 2.8.5
next: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
nextjs-node-loader@1.1.5(webpack@5.92.1):
dependencies:
@@ -18410,7 +18483,7 @@ snapshots:
dependencies:
chokidar: 3.6.0
immutable: 4.3.6
source-map-js: 1.2.0
source-map-js: 1.2.1
sax@1.4.1: {}
@@ -18755,6 +18828,11 @@ snapshots:
'@babel/core': 7.24.9
babel-plugin-macros: 3.1.0
styled-jsx@5.1.1(react@18.3.1):
dependencies:
client-only: 0.0.1
react: 18.3.1
stylis@4.2.0: {}
stylis@4.3.2: {}
@@ -18956,6 +19034,25 @@ snapshots:
ts-dedent@2.2.0: {}
ts-jest@29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3):
dependencies:
bs-logger: 0.2.6
ejs: 3.1.10
fast-json-stable-stringify: 2.1.0
jest: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3))
jest-util: 29.7.0
json5: 2.2.3
lodash.memoize: 4.1.2
make-error: 1.3.6
semver: 7.6.3
typescript: 5.5.3
yargs-parser: 21.1.1
optionalDependencies:
'@babel/core': 7.24.9
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.24.9)
ts-jest@29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0))(typescript@5.5.3):
dependencies:
bs-logger: 0.2.6

View File

@@ -42,6 +42,7 @@
"jest": "^29.5.0",
"js-yaml": "^4.1.0",
"json5": "^2.2.3",
"jsondiffpatch": "^0.6.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"mermaid": "^10.2.3",

View File

@@ -13,7 +13,11 @@ import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, WorkflowSnapshotsType } from '../WorkflowComponents/context';
import {
WorkflowContext,
WorkflowSnapshotsType,
WorkflowStateType
} from '../WorkflowComponents/context';
import { AppContext, TabEnum } from '../context';
import RouteTab from '../RouteTab';
import { useRouter } from 'next/router';
@@ -34,6 +38,7 @@ import {
WorkflowInitContext
} from '../WorkflowComponents/context/workflowInitContext';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
import { applyDiff } from '@/web/core/app/diff';
const Header = () => {
const { t } = useTranslation();
@@ -76,11 +81,14 @@ const Header = () => {
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
const initialState = past[past.length - 1]?.state;
const savedSnapshotState = applyDiff(initialState, savedSnapshot?.diff);
const val = compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
nodes: savedSnapshotState?.nodes,
edges: savedSnapshotState?.edges,
chatConfig: savedSnapshotState?.chatConfig
},
{
nodes: nodes,

View File

@@ -17,6 +17,7 @@ import styles from './styles.module.scss';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useTranslation } from 'next-i18next';
import { onSaveSnapshotFnType, SimpleAppSnapshotType } from './useSnapshots';
import { applyDiff } from '@/web/core/app/diff';
const Edit = ({
appForm,
@@ -39,16 +40,19 @@ const Edit = ({
// show selected dataset
loadAllDatasets();
// Get the latest snapshot
if (past?.[0]?.appForm) {
return setAppForm(past[0].appForm);
}
const appForm = appWorkflow2Form({
nodes: appDetail.modules,
chatConfig: appDetail.chatConfig
});
// Get the latest snapshot
if (past?.[0]?.diff) {
const pastState = applyDiff(past[past.length - 1].state, past[0].diff);
return setAppForm(pastState);
}
setAppForm(appForm);
// Set the first snapshot
if (past.length === 0) {
saveSnapshot({
@@ -58,8 +62,6 @@ const Edit = ({
});
}
setAppForm(appForm);
if (appDetail.version !== 'v2') {
setAppForm(
appWorkflow2Form({

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import FolderPath from '@/components/common/folder/Path';
@@ -29,6 +29,7 @@ import {
} from './useSnapshots';
import PublishHistories from '../PublishHistoriesSlider';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import { applyDiff } from '@/web/core/app/diff';
const Header = ({
forbiddenSaveSnapshot,
@@ -48,9 +49,20 @@ const Header = ({
const { t } = useTranslation();
const { isPc } = useSystem();
const router = useRouter();
const { appId, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v);
const { appId, onSaveApp, currentTab, appLatestVersion } = useContextSelector(
AppContext,
(v) => v
);
const { lastAppListRouteType } = useSystemStore();
const { allDatasets } = useDatasetStore();
const initialAppForm = useMemo(
() =>
appWorkflow2Form({
nodes: appLatestVersion?.nodes || [],
chatConfig: appLatestVersion?.chatConfig || {}
}),
[appLatestVersion]
);
const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), {
manual: false,
@@ -104,7 +116,8 @@ const Header = ({
const onSwitchTmpVersion = useCallback(
(data: SimpleAppSnapshotType, customTitle: string) => {
setAppForm(data.appForm);
const pastState = applyDiff(initialAppForm, data.diff);
setAppForm(pastState);
// Remove multiple "copy-"
const copyText = t('app:version_copy');
@@ -112,11 +125,11 @@ const Header = ({
const title = customTitle.replace(regex, `$1`);
return saveSnapshot({
appForm: data.appForm,
appForm: pastState,
title
});
},
[saveSnapshot, setAppForm, t]
[initialAppForm, saveSnapshot, setAppForm, t]
);
const onSwitchCloudVersion = useCallback(
(appVersion: AppVersionSchemaType) => {
@@ -143,7 +156,8 @@ const Header = ({
useDebounceEffect(
() => {
const savedSnapshot = past.find((snapshot) => snapshot.isSaved);
const val = compareSimpleAppSnapshot(savedSnapshot?.appForm, appForm);
const pastState = applyDiff(initialAppForm, savedSnapshot?.diff);
const val = compareSimpleAppSnapshot(pastState, appForm);
setIsPublished(val);
},
[past, allDatasets],

View File

@@ -3,11 +3,13 @@ import { SetStateAction, useEffect, useRef } from 'react';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { isEqual } from 'lodash';
import { applyDiff, createDiff } from '@/web/core/app/diff';
export type SimpleAppSnapshotType = {
appForm: AppSimpleEditFormType;
diff?: Record<string, any>;
title: string;
isSaved?: boolean;
state?: AppSimpleEditFormType;
};
export type onSaveSnapshotFnType = (props: {
appForm: AppSimpleEditFormType;
@@ -66,14 +68,32 @@ export const useSimpleAppSnapshots = (appId: string) => {
return false;
}
const pastState = past[0];
if (past.length === 0) {
setPast([
{
title: title || formatTime2YMDHMS(new Date()),
isSaved,
state: appForm
}
]);
return true;
}
const isPastEqual = compareSimpleAppSnapshot(pastState?.appForm, appForm);
if (isPastEqual) return false;
const initialState = past[past.length - 1].state;
if (!initialState) return false;
if (past.length > 0) {
const pastState = applyDiff(initialState, past[0].diff);
const isPastEqual = compareSimpleAppSnapshot(pastState, appForm);
if (isPastEqual) return false;
}
const diff = createDiff(initialState, appForm);
setPast((past) => [
{
appForm,
diff,
title: title || formatTime2YMDHMS(new Date()),
isSaved
},

View File

@@ -13,7 +13,7 @@ import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../WorkflowComponents/context';
import { WorkflowContext, WorkflowStateType } from '../WorkflowComponents/context';
import { AppContext, TabEnum } from '../context';
import RouteTab from '../RouteTab';
import { useRouter } from 'next/router';
@@ -34,6 +34,7 @@ import {
WorkflowInitContext
} from '../WorkflowComponents/context/workflowInitContext';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
import { applyDiff } from '@/web/core/app/diff';
const Header = () => {
const { t } = useTranslation();
@@ -81,11 +82,14 @@ const Header = () => {
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
const initialState = past[past.length - 1]?.state;
const savedSnapshotState = applyDiff(initialState, savedSnapshot?.diff);
const val = compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
nodes: savedSnapshotState?.nodes,
edges: savedSnapshotState?.edges,
chatConfig: savedSnapshotState?.chatConfig
},
{
nodes: nodes,

View File

@@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comment';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { useReactFlow } from 'reactflow';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';

View File

@@ -2,6 +2,7 @@ import { postWorkflowDebug } from '@/web/core/workflow/api';
import {
checkWorkflowNodeAndConnection,
compareSnapshot,
simplifyNodes,
storeEdgesRenderEdge,
storeNode2FlowNode
} from '@/web/core/workflow/utils';
@@ -41,6 +42,7 @@ import { cloneDeep } from 'lodash';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import WorkflowInitContextProvider, { WorkflowNodeEdgeContext } from './workflowInitContext';
import WorkflowEventContextProvider from './workflowEventContext';
import { applyDiff, createDiff } from '@/web/core/app/diff';
/*
Context
@@ -67,14 +69,22 @@ export const ReactFlowCustomProvider = ({
);
};
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
export type WorkflowSnapshotsType = {
diff?: any;
title: string;
isSaved?: boolean;
state?: WorkflowStateType;
// old format
nodes?: Node[];
edges?: Edge[];
chatConfig?: AppChatConfigType;
};
export type WorkflowStateType = {
nodes: Node[];
edges: Edge[];
title: string;
chatConfig: AppChatConfigType;
isSaved?: boolean;
};
type WorkflowContextType = {
@@ -751,7 +761,7 @@ const WorkflowContextProvider = ({
defaultValue: []
}) as [WorkflowSnapshotsType[], (value: SetStateAction<WorkflowSnapshotsType[]>) => void];
const resetSnapshot = useMemoizedFn((state: Omit<WorkflowSnapshotsType, 'title' | 'isSaved'>) => {
const resetSnapshot = useMemoizedFn((state: WorkflowStateType) => {
setNodes(state.nodes);
setEdges(state.edges);
setAppDetail((detail) => ({
@@ -759,20 +769,9 @@ const WorkflowContextProvider = ({
chatConfig: state.chatConfig
}));
});
const pushPastSnapshot = useMemoizedFn(
({
pastNodes,
pastEdges,
customTitle,
chatConfig,
isSaved
}: {
pastNodes: Node[];
pastEdges: Edge[];
customTitle?: string;
chatConfig: AppChatConfigType;
isSaved?: boolean;
}) => {
({ pastNodes, pastEdges, chatConfig, customTitle, isSaved }) => {
if (!pastNodes || !pastEdges || !chatConfig) return false;
if (forbiddenSaveSnapshot.current) {
@@ -780,7 +779,13 @@ const WorkflowContextProvider = ({
return false;
}
const pastState = past[0];
// Get initial state
const initialState = past[past.length - 1]?.state;
if (!initialState) return false;
// Apply latest diff to get past state
const pastState = applyDiff(initialState, past[0].diff);
const isPastEqual = compareSnapshot(
{
nodes: pastNodes,
@@ -796,13 +801,21 @@ const WorkflowContextProvider = ({
if (isPastEqual) return false;
// Create current state object
const newState = {
nodes: simplifyNodes(pastNodes),
edges: pastEdges,
chatConfig
};
// Calculate diff from initial state
const diff = createDiff(initialState, newState);
setFuture([]);
setPast((past) => [
{
nodes: pastNodes,
edges: pastEdges,
diff,
title: customTitle || formatTime2YMDHMS(new Date()),
chatConfig,
isSaved
},
...past.slice(0, 199)
@@ -811,18 +824,20 @@ const WorkflowContextProvider = ({
return true;
}
);
const onSwitchTmpVersion = useMemoizedFn((params: WorkflowSnapshotsType, customTitle: string) => {
// Remove multiple "copy-"
const copyText = t('app:version_copy');
const regex = new RegExp(`(${copyText}-)\\1+`, 'g');
const title = customTitle.replace(regex, `$1`);
const pastState = applyDiff(past[past.length - 1].state, params.diff);
resetSnapshot(params);
resetSnapshot(pastState);
return pushPastSnapshot({
pastNodes: params.nodes,
pastEdges: params.edges,
chatConfig: params.chatConfig,
pastNodes: pastState.nodes,
pastEdges: pastState.edges,
chatConfig: pastState.chatConfig,
customTitle: title
});
});
@@ -848,15 +863,19 @@ const WorkflowContextProvider = ({
if (past[1]) {
setFuture((future) => [past[0], ...future]);
setPast((past) => past.slice(1));
resetSnapshot(past[1]);
const pastState = applyDiff(past[past.length - 1].state, past[1].diff);
resetSnapshot(pastState);
}
});
const redo = useMemoizedFn(() => {
const futureState = future[0];
if (!future[0]) return;
const futureState = applyDiff(past[past.length - 1].state, future[0].diff);
if (futureState) {
setPast((past) => [future[0], ...past]);
setFuture((future) => future.slice(1));
resetSnapshot(futureState);
}
});
@@ -873,45 +892,113 @@ const WorkflowContextProvider = ({
});
}, [appId]);
const initData = useCallback(
async (e: Parameters<WorkflowContextType['initData']>[0], isInit?: boolean) => {
// Refresh web page, load init
if (isInit && past.length > 0) {
return resetSnapshot(past[0]);
// Convert old history format to new format
const convertOldFormatHistory = (past: WorkflowSnapshotsType[]) => {
const baseState = {
nodes: past[past.length - 1].state?.nodes || [],
edges: past[past.length - 1].state?.edges || [],
chatConfig: past[past.length - 1].state?.chatConfig || {}
};
return past.map((item, index) => {
if (index === past.length - 1) {
return {
title: item.title,
isSaved: item.isSaved,
state: baseState
};
}
// If it is the initial data, save the initial snapshot
const currentState = {
nodes: item.nodes || [],
edges: item.edges || [],
chatConfig: item.chatConfig || {}
};
const diff = createDiff(baseState, currentState);
return {
title: item.title || formatTime2YMDHMS(new Date()),
isSaved: item.isSaved,
diff
};
});
};
const initData = useCallback(
async (
e: {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
chatConfig?: AppChatConfigType;
},
isInit?: boolean
) => {
const nodes = e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [];
const edges = e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [];
const initialState = {
nodes: simplifyNodes(nodes),
edges,
chatConfig: e.chatConfig || appDetail.chatConfig
};
if (isInit && past.length > 0) {
// new format
if (past[0].diff) {
const targetState = applyDiff(
past[past.length - 1].state,
past[0].diff
) as WorkflowStateType;
setNodes(targetState.nodes);
setEdges(targetState.edges);
setAppDetail((state) => ({
...state,
chatConfig: targetState.chatConfig
}));
return;
}
// old format
if (past.some((item) => !item.state && (item.nodes || item.edges))) {
const newPast = convertOldFormatHistory(past);
setPast(newPast);
const latestState = applyDiff(
newPast[newPast.length - 1].state,
newPast[0].diff
) as WorkflowStateType;
setNodes(latestState.nodes);
setEdges(latestState.edges);
setAppDetail((state) => ({
...state,
chatConfig: latestState.chatConfig
}));
return;
}
}
setNodes(nodes);
setEdges(edges);
if (e.chatConfig) {
setAppDetail((state) => ({ ...state, chatConfig: e.chatConfig as AppChatConfigType }));
}
if (isInit && past.length === 0) {
pushPastSnapshot({
pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [],
pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [],
customTitle: t(`app:app.version_initial`),
chatConfig: appDetail.chatConfig,
isSaved: true
});
setPast([
{
title: t(`app:app.version_initial`),
isSaved: true,
state: initialState
}
]);
forbiddenSaveSnapshot.current = true;
}
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []);
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
const chatConfig = e.chatConfig;
if (chatConfig) {
setAppDetail((state) => ({
...state,
chatConfig
}));
}
},
[
appDetail.chatConfig,
past,
resetSnapshot,
pushPastSnapshot,
setAppDetail,
setEdges,
setNodes,
t
]
[appDetail.chatConfig, past, setAppDetail, setEdges, setNodes, setPast, t]
);
const value = useMemo(

View File

@@ -0,0 +1,20 @@
import { create } from 'jsondiffpatch';
const createWorkflowDiffPatcher = () =>
create({
objectHash: (obj: any) => obj.id || obj.nodeId || obj._id,
propertyFilter: (name: string) => name !== 'selected'
});
const diffPatcher = createWorkflowDiffPatcher();
export const createDiff = <T extends Record<string, unknown>>(initialState?: T, newState?: T) => {
return diffPatcher.diff(initialState, newState);
};
export const applyDiff = <T extends Record<string, unknown>>(
initialState?: T,
diff?: ReturnType<typeof diffPatcher.diff>
) => {
return diffPatcher.patch(structuredClone(initialState), diff) as T;
};

View File

@@ -631,3 +631,14 @@ export const compareSnapshot = (
return isEqual(node1, node2);
};
// remove node size
export const simplifyNodes = (nodes: Node[]) => {
return nodes.map((node) => ({
id: node.id,
type: node.type,
position: node.position,
data: node.data,
zIndex: node.zIndex
}));
};