From 3e421e47694d8142f8959d4818db629f03104050 Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Thu, 14 Nov 2024 14:19:30 +0800 Subject: [PATCH 1/4] refactor: snapshot store to diff --- pnpm-lock.yaml | 121 +++++++++++-- projects/app/package.json | 1 + .../app/detail/components/Plugin/Header.tsx | 26 ++- .../app/detail/components/SimpleApp/Edit.tsx | 26 ++- .../detail/components/SimpleApp/Header.tsx | 31 +++- .../components/SimpleApp/useSnapshots.tsx | 34 +++- .../app/detail/components/Workflow/Header.tsx | 22 ++- .../Flow/components/ContextMenu.tsx | 1 - .../WorkflowComponents/context/index.tsx | 171 +++++++++++------- 9 files changed, 330 insertions(+), 103 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b95fc1dc302..25297d3a116 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/projects/app/package.json b/projects/app/package.json index 344763d7581..5534e540889 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -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", diff --git a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx index 8df93e25890..2124a22355a 100644 --- a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx @@ -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,12 @@ import { WorkflowInitContext } from '../WorkflowComponents/context/workflowInitContext'; import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext'; +import { create } from 'jsondiffpatch'; + +const diffPatcher = create({ + objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, + propertyFilter: (name: string) => name !== 'selected' +}); const Header = () => { const { t } = useTranslation(); @@ -59,7 +69,8 @@ const Header = () => { future, setPast, onSwitchTmpVersion, - onSwitchCloudVersion + onSwitchCloudVersion, + initialState } = useContextSelector(WorkflowContext, (v) => v); const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal); const setShowHistoryModal = useContextSelector( @@ -76,11 +87,16 @@ const Header = () => { [...future].reverse().find((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved); + const savedSnapshotState = diffPatcher.patch( + structuredClone(initialState), + savedSnapshot?.diff + ) as WorkflowStateType; + const val = compareSnapshot( { - nodes: savedSnapshot?.nodes, - edges: savedSnapshot?.edges, - chatConfig: savedSnapshot?.chatConfig + nodes: savedSnapshotState?.nodes, + edges: savedSnapshotState?.edges, + chatConfig: savedSnapshotState?.chatConfig }, { nodes: nodes, diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx index 68af2e8960e..cb60466565b 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx @@ -16,7 +16,13 @@ import { cardStyles } from '../constants'; import styles from './styles.module.scss'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useTranslation } from 'next-i18next'; -import { onSaveSnapshotFnType, SimpleAppSnapshotType } from './useSnapshots'; +import { onSaveSnapshotFnType, SimpleAppSnapshotType, useSimpleAppSnapshots } from './useSnapshots'; +import { create } from 'jsondiffpatch'; + +const diffPatcher = create({ + objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, + propertyFilter: (name: string) => name !== 'selected' +}); const Edit = ({ appForm, @@ -39,16 +45,22 @@ 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 = diffPatcher.patch( + structuredClone(appForm), + past[0].diff + ) as AppSimpleEditFormType; + + return setAppForm(pastState); + } + + setAppForm(appForm); // Set the first snapshot if (past.length === 0) { saveSnapshot({ @@ -58,8 +70,6 @@ const Edit = ({ }); } - setAppForm(appForm); - if (appDetail.version !== 'v2') { setAppForm( appWorkflow2Form({ diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx index c52f4d5ffb9..28ed1f5acb0 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx @@ -29,6 +29,12 @@ import { } from './useSnapshots'; import PublishHistories from '../PublishHistoriesSlider'; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; +import { create } from 'jsondiffpatch'; + +const diffPatcher = create({ + objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, + propertyFilter: (name: string) => name !== 'selected' +}); const Header = ({ forbiddenSaveSnapshot, @@ -48,9 +54,16 @@ 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 = appWorkflow2Form({ + nodes: appLatestVersion?.nodes || [], + chatConfig: appLatestVersion?.chatConfig || {} + }); const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), { manual: false, @@ -104,7 +117,11 @@ const Header = ({ const onSwitchTmpVersion = useCallback( (data: SimpleAppSnapshotType, customTitle: string) => { - setAppForm(data.appForm); + const pastState = diffPatcher.patch( + structuredClone(initialAppForm), + data.diff + ) as AppSimpleEditFormType; + setAppForm(pastState); // Remove multiple "copy-" const copyText = t('app:version_copy'); @@ -112,11 +129,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 +160,11 @@ const Header = ({ useDebounceEffect( () => { const savedSnapshot = past.find((snapshot) => snapshot.isSaved); - const val = compareSimpleAppSnapshot(savedSnapshot?.appForm, appForm); + const pastState = diffPatcher.patch( + structuredClone(initialAppForm), + savedSnapshot?.diff + ) as AppSimpleEditFormType; + const val = compareSimpleAppSnapshot(pastState, appForm); setIsPublished(val); }, [past, allDatasets], diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx index 6443d410c44..7cf04ea8ae1 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx @@ -1,11 +1,20 @@ import { useLocalStorageState, useMemoizedFn } from 'ahooks'; -import { SetStateAction, useEffect, useRef } from 'react'; +import { SetStateAction, useEffect, useRef, useState } from 'react'; import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type'; import { isEqual } from 'lodash'; +import { create } from 'jsondiffpatch'; +import { useContextSelector } from 'use-context-selector'; +import { AppContext } from '../context'; +import { appWorkflow2Form } from '@fastgpt/global/core/app/utils'; + +const diffPatcher = create({ + objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, + propertyFilter: (name: string) => name !== 'selected' +}); export type SimpleAppSnapshotType = { - appForm: AppSimpleEditFormType; + diff?: any; title: string; isSaved?: boolean; }; @@ -59,6 +68,7 @@ export const useSimpleAppSnapshots = (appId: string) => { const [past, setPast] = useLocalStorageState(`${appId}-past-simple`, { defaultValue: [] }) as [SimpleAppSnapshotType[], (value: SetStateAction) => void]; + const { appLatestVersion } = useContextSelector(AppContext, (v) => v); const saveSnapshot: onSaveSnapshotFnType = useMemoizedFn(async ({ appForm, title, isSaved }) => { if (forbiddenSaveSnapshot.current) { @@ -66,14 +76,26 @@ export const useSimpleAppSnapshots = (appId: string) => { return false; } - const pastState = past[0]; + const initialAppForm = appWorkflow2Form({ + nodes: appLatestVersion?.nodes || [], + chatConfig: appLatestVersion?.chatConfig || {} + }); + + if (past.length > 0) { + const pastState = diffPatcher.patch( + structuredClone(initialAppForm), + past[0].diff + ) as AppSimpleEditFormType; + + const isPastEqual = compareSimpleAppSnapshot(pastState, appForm); + if (isPastEqual) return false; + } - const isPastEqual = compareSimpleAppSnapshot(pastState?.appForm, appForm); - if (isPastEqual) return false; + const diff = diffPatcher.diff(structuredClone(initialAppForm), appForm); setPast((past) => [ { - appForm, + diff, title: title || formatTime2YMDHMS(new Date()), isSaved }, diff --git a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx index 2e28c6d7d2a..e3f50f7c946 100644 --- a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx @@ -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,12 @@ import { WorkflowInitContext } from '../WorkflowComponents/context/workflowInitContext'; import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext'; +import { create } from 'jsondiffpatch'; + +const diffPatcher = create({ + objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, + propertyFilter: (name: string) => name !== 'selected' +}); const Header = () => { const { t } = useTranslation(); @@ -63,7 +69,8 @@ const Header = () => { future, setPast, onSwitchTmpVersion, - onSwitchCloudVersion + onSwitchCloudVersion, + initialState } = useContextSelector(WorkflowContext, (v) => v); const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal); const setShowHistoryModal = useContextSelector( @@ -81,11 +88,16 @@ const Header = () => { [...future].reverse().find((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved); + const savedSnapshotState = diffPatcher.patch( + structuredClone(initialState), + savedSnapshot?.diff + ) as WorkflowStateType; + const val = compareSnapshot( { - nodes: savedSnapshot?.nodes, - edges: savedSnapshot?.edges, - chatConfig: savedSnapshot?.chatConfig + nodes: savedSnapshotState?.nodes, + edges: savedSnapshotState?.edges, + chatConfig: savedSnapshotState?.chatConfig }, { nodes: nodes, diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ContextMenu.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ContextMenu.tsx index 1c62953207e..370e25f7fed 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ContextMenu.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ContextMenu.tsx @@ -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'; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx index 25ad6c2eab7..33d0cf5c9b4 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx @@ -41,6 +41,12 @@ import { cloneDeep } from 'lodash'; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; import WorkflowInitContextProvider, { WorkflowNodeEdgeContext } from './workflowInitContext'; import WorkflowEventContextProvider from './workflowEventContext'; +import { create } from 'jsondiffpatch'; + +const diffPatcher = create({ + objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, + propertyFilter: (name: string) => name !== 'selected' +}); /* Context @@ -67,20 +73,23 @@ export const ReactFlowCustomProvider = ({ ); }; -type OnChange = (changes: ChangesType[]) => void; - export type WorkflowSnapshotsType = { + diff?: any; + title: string; + isSaved?: boolean; +}; + +export type WorkflowStateType = { nodes: Node[]; edges: Edge[]; - title: string; chatConfig: AppChatConfigType; - isSaved?: boolean; }; type WorkflowContextType = { appId?: string; basicNodeTemplates: FlowNodeTemplateType[]; filterAppIds?: string[]; + initialState?: WorkflowStateType; // nodes nodeList: FlowNodeItemType[]; @@ -211,6 +220,11 @@ export const WorkflowContext = createContext({ onResetNode: function (e: { id: string; node: FlowNodeTemplateType }): void { throw new Error('Function not implemented.'); }, + initialState: { + nodes: [], + edges: [], + chatConfig: {} + }, onDelEdge: function (e: { nodeId: string; @@ -751,7 +765,7 @@ const WorkflowContextProvider = ({ defaultValue: [] }) as [WorkflowSnapshotsType[], (value: SetStateAction) => void]; - const resetSnapshot = useMemoizedFn((state: Omit) => { + const resetSnapshot = useMemoizedFn((state: WorkflowStateType) => { setNodes(state.nodes); setEdges(state.edges); setAppDetail((detail) => ({ @@ -759,28 +773,27 @@ const WorkflowContextProvider = ({ chatConfig: state.chatConfig })); }); + const pushPastSnapshot = useMemoizedFn( - ({ - pastNodes, - pastEdges, - customTitle, - chatConfig, - isSaved - }: { - pastNodes: Node[]; - pastEdges: Edge[]; - customTitle?: string; - chatConfig: AppChatConfigType; - isSaved?: boolean; - }) => { - if (!pastNodes || !pastEdges || !chatConfig) return false; + ({ pastNodes, pastEdges, chatConfig, customTitle, isSaved }) => { + if (!pastNodes || !pastEdges || !chatConfig || !initialState.current) return false; if (forbiddenSaveSnapshot.current) { forbiddenSaveSnapshot.current = false; return false; } - const pastState = past[0]; + const newState = { + nodes: pastNodes, + edges: pastEdges, + chatConfig + }; + + const pastState = diffPatcher.patch( + structuredClone(initialState.current), + past[0].diff + ) as WorkflowStateType; + const isPastEqual = compareSnapshot( { nodes: pastNodes, @@ -796,33 +809,39 @@ const WorkflowContextProvider = ({ if (isPastEqual) return false; + const diff = diffPatcher.diff(initialState.current, newState); + setFuture([]); setPast((past) => [ { - nodes: pastNodes, - edges: pastEdges, + diff, title: customTitle || formatTime2YMDHMS(new Date()), - chatConfig, isSaved }, - ...past.slice(0, 199) + ...past ]); 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`); - resetSnapshot(params); + const pastState = diffPatcher.patch( + structuredClone(initialState.current), + params.diff + ) as WorkflowStateType; + + 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 +867,26 @@ const WorkflowContextProvider = ({ if (past[1]) { setFuture((future) => [past[0], ...future]); setPast((past) => past.slice(1)); - resetSnapshot(past[1]); + + const pastState = diffPatcher.patch( + structuredClone(initialState.current), + past[1].diff + ) as WorkflowStateType; + resetSnapshot(pastState); } }); const redo = useMemoizedFn(() => { - const futureState = future[0]; + if (!future[0]) return; + + const futureState = diffPatcher.patch( + structuredClone(initialState.current), + future[0].diff + ) as WorkflowStateType; if (futureState) { setPast((past) => [future[0], ...past]); setFuture((future) => future.slice(1)); + resetSnapshot(futureState); } }); @@ -873,51 +903,70 @@ const WorkflowContextProvider = ({ }); }, [appId]); + const initialState = useRef<{ + nodes: Node[]; + edges: Edge[]; + chatConfig: AppChatConfigType; + }>(); + const initData = useCallback( - async (e: Parameters[0], isInit?: boolean) => { - // Refresh web page, load init - if (isInit && past.length > 0) { - return resetSnapshot(past[0]); - } - // If it is the initial data, save the initial snapshot - 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 - }); - forbiddenSaveSnapshot.current = true; - } + 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 })) || []; + + initialState.current = { + nodes, + edges, + chatConfig: e.chatConfig || appDetail.chatConfig + }; - setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []); - setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []); + // If initializing and has past snapshots, restore from latest snapshot + if (isInit && past.length > 0 && past[0].diff && initialState.current) { + const targetState = diffPatcher.patch( + structuredClone(initialState.current), + past[0].diff + ) as WorkflowStateType; - const chatConfig = e.chatConfig; - if (chatConfig) { + setNodes(targetState.nodes); + setEdges(targetState.edges); setAppDetail((state) => ({ ...state, - chatConfig + chatConfig: targetState.chatConfig })); + return; + } + + setNodes(nodes); + setEdges(edges); + if (e.chatConfig) { + setAppDetail((state) => ({ ...state, chatConfig: e.chatConfig as AppChatConfigType })); + } + + if (isInit && past.length === 0) { + setPast([ + { + title: t(`app:app.version_initial`), + isSaved: true + } + ]); + forbiddenSaveSnapshot.current = true; } }, - [ - appDetail.chatConfig, - past, - resetSnapshot, - pushPastSnapshot, - setAppDetail, - setEdges, - setNodes, - t - ] + [appDetail.chatConfig, past, setAppDetail, setEdges, setNodes, setPast, t] ); const value = useMemo( () => ({ appId, basicNodeTemplates, + initialState: initialState.current, // node nodeList, From 169aa39175b3f0a9736f08dcfc98ef293cea55be Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Thu, 14 Nov 2024 18:01:09 +0800 Subject: [PATCH 2/4] change initial state position --- .../app/detail/components/Plugin/Header.tsx | 4 +- .../app/detail/components/SimpleApp/Edit.tsx | 4 +- .../components/SimpleApp/useSnapshots.tsx | 26 ++++++----- .../app/detail/components/Workflow/Header.tsx | 4 +- .../WorkflowComponents/context/index.tsx | 43 ++++++++----------- projects/app/src/web/core/workflow/utils.ts | 18 ++++++++ 6 files changed, 58 insertions(+), 41 deletions(-) diff --git a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx index 2124a22355a..3adedde38fe 100644 --- a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx @@ -69,8 +69,7 @@ const Header = () => { future, setPast, onSwitchTmpVersion, - onSwitchCloudVersion, - initialState + onSwitchCloudVersion } = useContextSelector(WorkflowContext, (v) => v); const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal); const setShowHistoryModal = useContextSelector( @@ -87,6 +86,7 @@ const Header = () => { [...future].reverse().find((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved); + const initialState = past[past.length - 1]?.state; const savedSnapshotState = diffPatcher.patch( structuredClone(initialState), savedSnapshot?.diff diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx index cb60466565b..d87c5fb654a 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx @@ -16,7 +16,7 @@ import { cardStyles } from '../constants'; import styles from './styles.module.scss'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useTranslation } from 'next-i18next'; -import { onSaveSnapshotFnType, SimpleAppSnapshotType, useSimpleAppSnapshots } from './useSnapshots'; +import { onSaveSnapshotFnType, SimpleAppSnapshotType } from './useSnapshots'; import { create } from 'jsondiffpatch'; const diffPatcher = create({ @@ -53,7 +53,7 @@ const Edit = ({ // Get the latest snapshot if (past?.[0]?.diff) { const pastState = diffPatcher.patch( - structuredClone(appForm), + structuredClone(past[past.length - 1].state), past[0].diff ) as AppSimpleEditFormType; diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx index 7cf04ea8ae1..a58af6b3378 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx @@ -4,9 +4,6 @@ import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type'; import { isEqual } from 'lodash'; import { create } from 'jsondiffpatch'; -import { useContextSelector } from 'use-context-selector'; -import { AppContext } from '../context'; -import { appWorkflow2Form } from '@fastgpt/global/core/app/utils'; const diffPatcher = create({ objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, @@ -17,6 +14,7 @@ export type SimpleAppSnapshotType = { diff?: any; title: string; isSaved?: boolean; + state?: AppSimpleEditFormType; }; export type onSaveSnapshotFnType = (props: { appForm: AppSimpleEditFormType; @@ -68,7 +66,6 @@ export const useSimpleAppSnapshots = (appId: string) => { const [past, setPast] = useLocalStorageState(`${appId}-past-simple`, { defaultValue: [] }) as [SimpleAppSnapshotType[], (value: SetStateAction) => void]; - const { appLatestVersion } = useContextSelector(AppContext, (v) => v); const saveSnapshot: onSaveSnapshotFnType = useMemoizedFn(async ({ appForm, title, isSaved }) => { if (forbiddenSaveSnapshot.current) { @@ -76,14 +73,23 @@ export const useSimpleAppSnapshots = (appId: string) => { return false; } - const initialAppForm = appWorkflow2Form({ - nodes: appLatestVersion?.nodes || [], - chatConfig: appLatestVersion?.chatConfig || {} - }); + if (past.length === 0) { + setPast([ + { + title: title || formatTime2YMDHMS(new Date()), + isSaved, + state: appForm + } + ]); + return true; + } + + const initialState = past[past.length - 1].state; + if (!initialState) return false; if (past.length > 0) { const pastState = diffPatcher.patch( - structuredClone(initialAppForm), + structuredClone(initialState), past[0].diff ) as AppSimpleEditFormType; @@ -91,7 +97,7 @@ export const useSimpleAppSnapshots = (appId: string) => { if (isPastEqual) return false; } - const diff = diffPatcher.diff(structuredClone(initialAppForm), appForm); + const diff = diffPatcher.diff(structuredClone(initialState), appForm); setPast((past) => [ { diff --git a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx index e3f50f7c946..ec7feb74b36 100644 --- a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx @@ -69,8 +69,7 @@ const Header = () => { future, setPast, onSwitchTmpVersion, - onSwitchCloudVersion, - initialState + onSwitchCloudVersion } = useContextSelector(WorkflowContext, (v) => v); const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal); const setShowHistoryModal = useContextSelector( @@ -88,6 +87,7 @@ const Header = () => { [...future].reverse().find((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved); + const initialState = past[past.length - 1]?.state; const savedSnapshotState = diffPatcher.patch( structuredClone(initialState), savedSnapshot?.diff diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx index 33d0cf5c9b4..ba7b7429086 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx @@ -2,6 +2,7 @@ import { postWorkflowDebug } from '@/web/core/workflow/api'; import { checkWorkflowNodeAndConnection, compareSnapshot, + simplifyNodes, storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils'; @@ -77,6 +78,7 @@ export type WorkflowSnapshotsType = { diff?: any; title: string; isSaved?: boolean; + state?: WorkflowStateType; }; export type WorkflowStateType = { @@ -89,7 +91,6 @@ type WorkflowContextType = { appId?: string; basicNodeTemplates: FlowNodeTemplateType[]; filterAppIds?: string[]; - initialState?: WorkflowStateType; // nodes nodeList: FlowNodeItemType[]; @@ -220,11 +221,6 @@ export const WorkflowContext = createContext({ onResetNode: function (e: { id: string; node: FlowNodeTemplateType }): void { throw new Error('Function not implemented.'); }, - initialState: { - nodes: [], - edges: [], - chatConfig: {} - }, onDelEdge: function (e: { nodeId: string; @@ -776,7 +772,7 @@ const WorkflowContextProvider = ({ const pushPastSnapshot = useMemoizedFn( ({ pastNodes, pastEdges, chatConfig, customTitle, isSaved }) => { - if (!pastNodes || !pastEdges || !chatConfig || !initialState.current) return false; + if (!pastNodes || !pastEdges || !chatConfig) return false; if (forbiddenSaveSnapshot.current) { forbiddenSaveSnapshot.current = false; @@ -784,13 +780,16 @@ const WorkflowContextProvider = ({ } const newState = { - nodes: pastNodes, + nodes: simplifyNodes(pastNodes), edges: pastEdges, chatConfig }; + const initialState = past[past.length - 1]?.state; + if (!initialState) return false; + const pastState = diffPatcher.patch( - structuredClone(initialState.current), + structuredClone(initialState), past[0].diff ) as WorkflowStateType; @@ -809,7 +808,7 @@ const WorkflowContextProvider = ({ if (isPastEqual) return false; - const diff = diffPatcher.diff(initialState.current, newState); + const diff = diffPatcher.diff(initialState, newState); setFuture([]); setPast((past) => [ @@ -832,7 +831,7 @@ const WorkflowContextProvider = ({ const title = customTitle.replace(regex, `$1`); const pastState = diffPatcher.patch( - structuredClone(initialState.current), + structuredClone(past[past.length - 1].state), params.diff ) as WorkflowStateType; @@ -869,7 +868,7 @@ const WorkflowContextProvider = ({ setPast((past) => past.slice(1)); const pastState = diffPatcher.patch( - structuredClone(initialState.current), + structuredClone(past[past.length - 1].state), past[1].diff ) as WorkflowStateType; resetSnapshot(pastState); @@ -879,7 +878,7 @@ const WorkflowContextProvider = ({ if (!future[0]) return; const futureState = diffPatcher.patch( - structuredClone(initialState.current), + structuredClone(past[past.length - 1].state), future[0].diff ) as WorkflowStateType; @@ -903,12 +902,6 @@ const WorkflowContextProvider = ({ }); }, [appId]); - const initialState = useRef<{ - nodes: Node[]; - edges: Edge[]; - chatConfig: AppChatConfigType; - }>(); - const initData = useCallback( async ( e: { @@ -921,16 +914,16 @@ const WorkflowContextProvider = ({ const nodes = e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []; const edges = e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []; - initialState.current = { - nodes, + const initialState = { + nodes: simplifyNodes(nodes), edges, chatConfig: e.chatConfig || appDetail.chatConfig }; // If initializing and has past snapshots, restore from latest snapshot - if (isInit && past.length > 0 && past[0].diff && initialState.current) { + if (isInit && past.length > 0 && past[0].diff) { const targetState = diffPatcher.patch( - structuredClone(initialState.current), + structuredClone(past[past.length - 1].state), past[0].diff ) as WorkflowStateType; @@ -953,7 +946,8 @@ const WorkflowContextProvider = ({ setPast([ { title: t(`app:app.version_initial`), - isSaved: true + isSaved: true, + state: initialState } ]); forbiddenSaveSnapshot.current = true; @@ -966,7 +960,6 @@ const WorkflowContextProvider = ({ () => ({ appId, basicNodeTemplates, - initialState: initialState.current, // node nodeList, diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index 5f2a21c94cd..3a8c81b81a6 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -631,3 +631,21 @@ export const compareSnapshot = ( return isEqual(node1, node2); }; + +// Simplify nodes, remove system config node & node size +export const simplifyNodes = (nodes: Node[]) => { + return nodes + .filter((node) => { + if (!node) return; + if (FlowNodeTypeEnum.systemConfig === node.type) return; + + return true; + }) + .map((node) => ({ + id: node.id, + type: node.type, + position: node.position, + data: node.data, + zIndex: node.zIndex + })); +}; From e40dfa560b4b0e011aedae3f1b46ec6b0df31c46 Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Fri, 15 Nov 2024 14:58:06 +0800 Subject: [PATCH 3/4] fix old snapshot format --- .../WorkflowComponents/context/index.tsx | 87 ++++++++++++++++--- projects/app/src/web/core/workflow/utils.ts | 23 ++--- 2 files changed, 81 insertions(+), 29 deletions(-) diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx index ba7b7429086..d12940bb5ee 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx @@ -79,6 +79,11 @@ export type WorkflowSnapshotsType = { title: string; isSaved?: boolean; state?: WorkflowStateType; + + // old format + nodes?: Node[]; + edges?: Edge[]; + chatConfig?: AppChatConfigType; }; export type WorkflowStateType = { @@ -902,6 +907,38 @@ const WorkflowContextProvider = ({ }); }, [appId]); + 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 + }; + } + + const currentState = { + nodes: item.nodes || [], + edges: item.edges || [], + chatConfig: item.chatConfig || {} + }; + + const diff = diffPatcher.diff(baseState, currentState); + + return { + title: item.title || formatTime2YMDHMS(new Date()), + isSaved: item.isSaved, + diff + }; + }); + }; + const initData = useCallback( async ( e: { @@ -920,20 +957,42 @@ const WorkflowContextProvider = ({ chatConfig: e.chatConfig || appDetail.chatConfig }; - // If initializing and has past snapshots, restore from latest snapshot - if (isInit && past.length > 0 && past[0].diff) { - const targetState = diffPatcher.patch( - structuredClone(past[past.length - 1].state), - past[0].diff - ) as WorkflowStateType; - - setNodes(targetState.nodes); - setEdges(targetState.edges); - setAppDetail((state) => ({ - ...state, - chatConfig: targetState.chatConfig - })); - return; + if (isInit && past.length > 0) { + // new format + if (past[0].diff) { + const targetState = diffPatcher.patch( + structuredClone(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 = diffPatcher.patch( + structuredClone(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); diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index 3a8c81b81a6..255c79cb0b6 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -632,20 +632,13 @@ export const compareSnapshot = ( return isEqual(node1, node2); }; -// Simplify nodes, remove system config node & node size +// remove node size export const simplifyNodes = (nodes: Node[]) => { - return nodes - .filter((node) => { - if (!node) return; - if (FlowNodeTypeEnum.systemConfig === node.type) return; - - return true; - }) - .map((node) => ({ - id: node.id, - type: node.type, - position: node.position, - data: node.data, - zIndex: node.zIndex - })); + return nodes.map((node) => ({ + id: node.id, + type: node.type, + position: node.position, + data: node.data, + zIndex: node.zIndex + })); }; From 7f210c5e5edc6a9e1a01679fda17599e59698892 Mon Sep 17 00:00:00 2001 From: heheer <1239331448@qq.com> Date: Tue, 19 Nov 2024 11:27:54 +0800 Subject: [PATCH 4/4] encapsulate json diff --- .../app/detail/components/Plugin/Header.tsx | 12 +--- .../app/detail/components/SimpleApp/Edit.tsx | 12 +--- .../detail/components/SimpleApp/Header.tsx | 31 ++++------ .../components/SimpleApp/useSnapshots.tsx | 18 ++---- .../app/detail/components/Workflow/Header.tsx | 12 +--- .../WorkflowComponents/context/index.tsx | 60 +++++++------------ projects/app/src/web/core/app/diff.ts | 20 +++++++ 7 files changed, 66 insertions(+), 99 deletions(-) create mode 100644 projects/app/src/web/core/app/diff.ts diff --git a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx index 3adedde38fe..edee7c1c175 100644 --- a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx @@ -38,12 +38,7 @@ import { WorkflowInitContext } from '../WorkflowComponents/context/workflowInitContext'; import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext'; -import { create } from 'jsondiffpatch'; - -const diffPatcher = create({ - objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, - propertyFilter: (name: string) => name !== 'selected' -}); +import { applyDiff } from '@/web/core/app/diff'; const Header = () => { const { t } = useTranslation(); @@ -87,10 +82,7 @@ const Header = () => { past.find((snapshot) => snapshot.isSaved); const initialState = past[past.length - 1]?.state; - const savedSnapshotState = diffPatcher.patch( - structuredClone(initialState), - savedSnapshot?.diff - ) as WorkflowStateType; + const savedSnapshotState = applyDiff(initialState, savedSnapshot?.diff); const val = compareSnapshot( { diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx index d87c5fb654a..df06c48112c 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx @@ -17,12 +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 { create } from 'jsondiffpatch'; - -const diffPatcher = create({ - objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, - propertyFilter: (name: string) => name !== 'selected' -}); +import { applyDiff } from '@/web/core/app/diff'; const Edit = ({ appForm, @@ -52,10 +47,7 @@ const Edit = ({ // Get the latest snapshot if (past?.[0]?.diff) { - const pastState = diffPatcher.patch( - structuredClone(past[past.length - 1].state), - past[0].diff - ) as AppSimpleEditFormType; + const pastState = applyDiff(past[past.length - 1].state, past[0].diff); return setAppForm(pastState); } diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx index 28ed1f5acb0..d0f988657df 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx @@ -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,12 +29,7 @@ import { } from './useSnapshots'; import PublishHistories from '../PublishHistoriesSlider'; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; -import { create } from 'jsondiffpatch'; - -const diffPatcher = create({ - objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, - propertyFilter: (name: string) => name !== 'selected' -}); +import { applyDiff } from '@/web/core/app/diff'; const Header = ({ forbiddenSaveSnapshot, @@ -60,10 +55,14 @@ const Header = ({ ); const { lastAppListRouteType } = useSystemStore(); const { allDatasets } = useDatasetStore(); - const initialAppForm = appWorkflow2Form({ - nodes: appLatestVersion?.nodes || [], - chatConfig: appLatestVersion?.chatConfig || {} - }); + const initialAppForm = useMemo( + () => + appWorkflow2Form({ + nodes: appLatestVersion?.nodes || [], + chatConfig: appLatestVersion?.chatConfig || {} + }), + [appLatestVersion] + ); const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), { manual: false, @@ -117,10 +116,7 @@ const Header = ({ const onSwitchTmpVersion = useCallback( (data: SimpleAppSnapshotType, customTitle: string) => { - const pastState = diffPatcher.patch( - structuredClone(initialAppForm), - data.diff - ) as AppSimpleEditFormType; + const pastState = applyDiff(initialAppForm, data.diff); setAppForm(pastState); // Remove multiple "copy-" @@ -160,10 +156,7 @@ const Header = ({ useDebounceEffect( () => { const savedSnapshot = past.find((snapshot) => snapshot.isSaved); - const pastState = diffPatcher.patch( - structuredClone(initialAppForm), - savedSnapshot?.diff - ) as AppSimpleEditFormType; + const pastState = applyDiff(initialAppForm, savedSnapshot?.diff); const val = compareSimpleAppSnapshot(pastState, appForm); setIsPublished(val); }, diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx index a58af6b3378..600cd013c41 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/useSnapshots.tsx @@ -1,17 +1,12 @@ import { useLocalStorageState, useMemoizedFn } from 'ahooks'; -import { SetStateAction, useEffect, useRef, useState } from 'react'; +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 { create } from 'jsondiffpatch'; - -const diffPatcher = create({ - objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, - propertyFilter: (name: string) => name !== 'selected' -}); +import { applyDiff, createDiff } from '@/web/core/app/diff'; export type SimpleAppSnapshotType = { - diff?: any; + diff?: Record; title: string; isSaved?: boolean; state?: AppSimpleEditFormType; @@ -88,16 +83,13 @@ export const useSimpleAppSnapshots = (appId: string) => { if (!initialState) return false; if (past.length > 0) { - const pastState = diffPatcher.patch( - structuredClone(initialState), - past[0].diff - ) as AppSimpleEditFormType; + const pastState = applyDiff(initialState, past[0].diff); const isPastEqual = compareSimpleAppSnapshot(pastState, appForm); if (isPastEqual) return false; } - const diff = diffPatcher.diff(structuredClone(initialState), appForm); + const diff = createDiff(initialState, appForm); setPast((past) => [ { diff --git a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx index ec7feb74b36..c72888816ba 100644 --- a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx @@ -34,12 +34,7 @@ import { WorkflowInitContext } from '../WorkflowComponents/context/workflowInitContext'; import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext'; -import { create } from 'jsondiffpatch'; - -const diffPatcher = create({ - objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, - propertyFilter: (name: string) => name !== 'selected' -}); +import { applyDiff } from '@/web/core/app/diff'; const Header = () => { const { t } = useTranslation(); @@ -88,10 +83,7 @@ const Header = () => { past.find((snapshot) => snapshot.isSaved); const initialState = past[past.length - 1]?.state; - const savedSnapshotState = diffPatcher.patch( - structuredClone(initialState), - savedSnapshot?.diff - ) as WorkflowStateType; + const savedSnapshotState = applyDiff(initialState, savedSnapshot?.diff); const val = compareSnapshot( { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx index d12940bb5ee..04c97049d7e 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx @@ -42,12 +42,7 @@ import { cloneDeep } from 'lodash'; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; import WorkflowInitContextProvider, { WorkflowNodeEdgeContext } from './workflowInitContext'; import WorkflowEventContextProvider from './workflowEventContext'; -import { create } from 'jsondiffpatch'; - -const diffPatcher = create({ - objectHash: (obj: any) => obj.id || obj.nodeId || obj._id, - propertyFilter: (name: string) => name !== 'selected' -}); +import { applyDiff, createDiff } from '@/web/core/app/diff'; /* Context @@ -784,19 +779,12 @@ const WorkflowContextProvider = ({ return false; } - const newState = { - nodes: simplifyNodes(pastNodes), - edges: pastEdges, - chatConfig - }; - + // Get initial state const initialState = past[past.length - 1]?.state; if (!initialState) return false; - const pastState = diffPatcher.patch( - structuredClone(initialState), - past[0].diff - ) as WorkflowStateType; + // Apply latest diff to get past state + const pastState = applyDiff(initialState, past[0].diff); const isPastEqual = compareSnapshot( { @@ -813,7 +801,15 @@ const WorkflowContextProvider = ({ if (isPastEqual) return false; - const diff = diffPatcher.diff(initialState, newState); + // 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) => [ @@ -822,7 +818,7 @@ const WorkflowContextProvider = ({ title: customTitle || formatTime2YMDHMS(new Date()), isSaved }, - ...past + ...past.slice(0, 199) ]); return true; @@ -834,11 +830,7 @@ const WorkflowContextProvider = ({ const copyText = t('app:version_copy'); const regex = new RegExp(`(${copyText}-)\\1+`, 'g'); const title = customTitle.replace(regex, `$1`); - - const pastState = diffPatcher.patch( - structuredClone(past[past.length - 1].state), - params.diff - ) as WorkflowStateType; + const pastState = applyDiff(past[past.length - 1].state, params.diff); resetSnapshot(pastState); @@ -871,21 +863,14 @@ const WorkflowContextProvider = ({ if (past[1]) { setFuture((future) => [past[0], ...future]); setPast((past) => past.slice(1)); - - const pastState = diffPatcher.patch( - structuredClone(past[past.length - 1].state), - past[1].diff - ) as WorkflowStateType; + const pastState = applyDiff(past[past.length - 1].state, past[1].diff); resetSnapshot(pastState); } }); const redo = useMemoizedFn(() => { if (!future[0]) return; - const futureState = diffPatcher.patch( - structuredClone(past[past.length - 1].state), - future[0].diff - ) as WorkflowStateType; + const futureState = applyDiff(past[past.length - 1].state, future[0].diff); if (futureState) { setPast((past) => [future[0], ...past]); @@ -907,6 +892,7 @@ const WorkflowContextProvider = ({ }); }, [appId]); + // Convert old history format to new format const convertOldFormatHistory = (past: WorkflowSnapshotsType[]) => { const baseState = { nodes: past[past.length - 1].state?.nodes || [], @@ -929,7 +915,7 @@ const WorkflowContextProvider = ({ chatConfig: item.chatConfig || {} }; - const diff = diffPatcher.diff(baseState, currentState); + const diff = createDiff(baseState, currentState); return { title: item.title || formatTime2YMDHMS(new Date()), @@ -960,8 +946,8 @@ const WorkflowContextProvider = ({ if (isInit && past.length > 0) { // new format if (past[0].diff) { - const targetState = diffPatcher.patch( - structuredClone(past[past.length - 1].state), + const targetState = applyDiff( + past[past.length - 1].state, past[0].diff ) as WorkflowStateType; @@ -980,8 +966,8 @@ const WorkflowContextProvider = ({ setPast(newPast); - const latestState = diffPatcher.patch( - structuredClone(newPast[newPast.length - 1].state), + const latestState = applyDiff( + newPast[newPast.length - 1].state, newPast[0].diff ) as WorkflowStateType; diff --git a/projects/app/src/web/core/app/diff.ts b/projects/app/src/web/core/app/diff.ts new file mode 100644 index 00000000000..6460b59e904 --- /dev/null +++ b/projects/app/src/web/core/app/diff.ts @@ -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 = >(initialState?: T, newState?: T) => { + return diffPatcher.diff(initialState, newState); +}; + +export const applyDiff = >( + initialState?: T, + diff?: ReturnType +) => { + return diffPatcher.patch(structuredClone(initialState), diff) as T; +};