balibabu commited on
Commit
bdb8bf3
·
1 Parent(s): 9741edc

feat: Automatically save agent page data #3301 (#3302)

Browse files

### What problem does this PR solve?

feat: Automatically save agent page data #3301

### Type of change


- [x] New Feature (non-breaking change which adds functionality)

web/src/locales/en.ts CHANGED
@@ -1036,6 +1036,7 @@ The above is the content you need to summarize.`,
1036
  howUseId: 'How to use agent ID?',
1037
  content: 'Content',
1038
  operationResults: 'Operation Results',
 
1039
  },
1040
  footer: {
1041
  profile: 'All rights reserved @ React',
 
1036
  howUseId: 'How to use agent ID?',
1037
  content: 'Content',
1038
  operationResults: 'Operation Results',
1039
+ autosave: 'Automatically saved',
1040
  },
1041
  footer: {
1042
  profile: 'All rights reserved @ React',
web/src/locales/zh-traditional.ts CHANGED
@@ -984,6 +984,7 @@ export default {
984
  howUseId: '如何使用Agent ID?',
985
  content: '內容',
986
  operationResults: '運行結果',
 
987
  },
988
  footer: {
989
  profile: '“保留所有權利 @ react”',
 
984
  howUseId: '如何使用Agent ID?',
985
  content: '內容',
986
  operationResults: '運行結果',
987
+ autosave: '已自動儲存',
988
  },
989
  footer: {
990
  profile: '“保留所有權利 @ react”',
web/src/locales/zh.ts CHANGED
@@ -1004,6 +1004,7 @@ export default {
1004
  howUseId: '如何使用Agent ID?',
1005
  content: '内容',
1006
  operationResults: '运行结果',
 
1007
  },
1008
  footer: {
1009
  profile: 'All rights reserved @ React',
 
1004
  howUseId: '如何使用Agent ID?',
1005
  content: '内容',
1006
  operationResults: '运行结果',
1007
+ autosave: '已自动保存',
1008
  },
1009
  footer: {
1010
  profile: 'All rights reserved @ React',
web/src/pages/flow/canvas/index.tsx CHANGED
@@ -128,6 +128,9 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
128
  onSelectionChange={onSelectionChange}
129
  nodeOrigin={[0.5, 0]}
130
  isValidConnection={isValidConnection}
 
 
 
131
  onChange={(...params) => {
132
  console.info('params:', ...params);
133
  }}
@@ -140,18 +143,6 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
140
  },
141
  }}
142
  deleteKeyCode={['Delete', 'Backspace']}
143
- onPaste={(...params) => {
144
- console.info('onPaste:', ...params);
145
- }}
146
- onPasteCapture={(...params) => {
147
- console.info('onPasteCapture:', ...params);
148
- }}
149
- onCopy={(...params) => {
150
- console.info('onCopy:', ...params);
151
- }}
152
- onCopyCapture={(...params) => {
153
- console.info('onCopyCapture:', ...params);
154
- }}
155
  >
156
  <Background />
157
  <Controls />
 
128
  onSelectionChange={onSelectionChange}
129
  nodeOrigin={[0.5, 0]}
130
  isValidConnection={isValidConnection}
131
+ onChangeCapture={(...params) => {
132
+ console.info('onChangeCapture:', ...params);
133
+ }}
134
  onChange={(...params) => {
135
  console.info('params:', ...params);
136
  }}
 
143
  },
144
  }}
145
  deleteKeyCode={['Delete', 'Backspace']}
 
 
 
 
 
 
 
 
 
 
 
 
146
  >
147
  <Background />
148
  <Controls />
web/src/pages/flow/canvas/node/relevant-node.tsx CHANGED
@@ -5,12 +5,15 @@ import { NodeData } from '../../interface';
5
  import { RightHandleStyle } from './handle-icon';
6
 
7
  import { get } from 'lodash';
 
8
  import styles from './index.less';
9
  import NodeHeader from './node-header';
10
 
11
  export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) {
12
  const yes = get(data, 'form.yes');
13
  const no = get(data, 'form.no');
 
 
14
  return (
15
  <section
16
  className={classNames(styles.logicNode, {
@@ -50,11 +53,11 @@ export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) {
50
  <Flex vertical gap={10}>
51
  <Flex vertical>
52
  <div className={styles.relevantLabel}>Yes</div>
53
- <div className={styles.nodeText}>{yes}</div>
54
  </Flex>
55
  <Flex vertical>
56
  <div className={styles.relevantLabel}>No</div>
57
- <div className={styles.nodeText}>{no}</div>
58
  </Flex>
59
  </Flex>
60
  </section>
 
5
  import { RightHandleStyle } from './handle-icon';
6
 
7
  import { get } from 'lodash';
8
+ import { useReplaceIdWithName } from '../../hooks';
9
  import styles from './index.less';
10
  import NodeHeader from './node-header';
11
 
12
  export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) {
13
  const yes = get(data, 'form.yes');
14
  const no = get(data, 'form.no');
15
+ const replaceIdWithName = useReplaceIdWithName();
16
+
17
  return (
18
  <section
19
  className={classNames(styles.logicNode, {
 
53
  <Flex vertical gap={10}>
54
  <Flex vertical>
55
  <div className={styles.relevantLabel}>Yes</div>
56
+ <div className={styles.nodeText}>{replaceIdWithName(yes)}</div>
57
  </Flex>
58
  <Flex vertical>
59
  <div className={styles.relevantLabel}>No</div>
60
+ <div className={styles.nodeText}>{replaceIdWithName(no)}</div>
61
  </Flex>
62
  </Flex>
63
  </section>
web/src/pages/flow/header/index.less CHANGED
@@ -1,3 +1,3 @@
1
  .flowHeader {
2
- padding: 0 20px;
3
  }
 
1
  .flowHeader {
2
+ padding: 10px 20px;
3
  }
web/src/pages/flow/header/index.tsx CHANGED
@@ -5,7 +5,11 @@ import { ArrowLeftOutlined } from '@ant-design/icons';
5
  import { Button, Flex, Space } from 'antd';
6
  import { Link, useParams } from 'umi';
7
  import FlowIdModal from '../flow-id-modal';
8
- import { useSaveGraph, useSaveGraphBeforeOpeningDebugDrawer } from '../hooks';
 
 
 
 
9
  import styles from './index.less';
10
 
11
  interface IProps {
@@ -20,10 +24,11 @@ const FlowHeader = ({ showChatDrawer }: IProps) => {
20
  const {
21
  visible: overviewVisible,
22
  hideModal: hideOverviewModal,
23
- showModal: showOverviewModal,
24
  } = useSetModalState();
25
  const { visible, hideModal, showModal } = useSetModalState();
26
  const { id } = useParams();
 
27
 
28
  return (
29
  <>
@@ -37,7 +42,10 @@ const FlowHeader = ({ showChatDrawer }: IProps) => {
37
  <Link to={`/flow`}>
38
  <ArrowLeftOutlined />
39
  </Link>
40
- <h3>{data.title}</h3>
 
 
 
41
  </Space>
42
  <Space size={'large'}>
43
  <Button onClick={handleRun}>
 
5
  import { Button, Flex, Space } from 'antd';
6
  import { Link, useParams } from 'umi';
7
  import FlowIdModal from '../flow-id-modal';
8
+ import {
9
+ useSaveGraph,
10
+ useSaveGraphBeforeOpeningDebugDrawer,
11
+ useWatchAgentChange,
12
+ } from '../hooks';
13
  import styles from './index.less';
14
 
15
  interface IProps {
 
24
  const {
25
  visible: overviewVisible,
26
  hideModal: hideOverviewModal,
27
+ // showModal: showOverviewModal,
28
  } = useSetModalState();
29
  const { visible, hideModal, showModal } = useSetModalState();
30
  const { id } = useParams();
31
+ const time = useWatchAgentChange();
32
 
33
  return (
34
  <>
 
42
  <Link to={`/flow`}>
43
  <ArrowLeftOutlined />
44
  </Link>
45
+ <div className="flex flex-col">
46
+ <span className="font-semibold text-[18px]">{data.title}</span>
47
+ <span className="font-normal text-sm">已自动保存 {time}</span>
48
+ </div>
49
  </Space>
50
  <Space size={'large'}>
51
  <Button onClick={handleRun}>
web/src/pages/flow/hooks.ts CHANGED
@@ -19,7 +19,9 @@ import {
19
  import { useFetchModelId, useSendMessageWithSse } from '@/hooks/logic-hooks';
20
  import { Variable } from '@/interfaces/database/chat';
21
  import api from '@/utils/api';
 
22
  import { FormInstance, message } from 'antd';
 
23
  import { humanId } from 'human-id';
24
  import { lowerFirst } from 'lodash';
25
  import trim from 'lodash/trim';
@@ -446,12 +448,21 @@ export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => {
446
  return handleRun;
447
  };
448
 
449
- export const useReplaceIdWithText = (output: unknown) => {
450
  const getNode = useGraphStore((state) => state.getNode);
451
 
452
- const getNameById = (id?: string) => {
453
- return getNode(id)?.data.name;
454
- };
 
 
 
 
 
 
 
 
 
455
 
456
  return {
457
  replacedOutput: replaceIdWithText(output, getNameById),
@@ -547,6 +558,7 @@ export const useWatchNodeFormDataChange = () => {
547
  );
548
 
549
  useEffect(() => {
 
550
  nodes.forEach((node) => {
551
  const currentNode = getNode(node.id);
552
  const form = currentNode?.data.form ?? {};
@@ -668,3 +680,36 @@ export const useCopyPaste = () => {
668
  };
669
  }, [onPasteCapture]);
670
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  import { useFetchModelId, useSendMessageWithSse } from '@/hooks/logic-hooks';
20
  import { Variable } from '@/interfaces/database/chat';
21
  import api from '@/utils/api';
22
+ import { useDebounceEffect } from 'ahooks';
23
  import { FormInstance, message } from 'antd';
24
+ import dayjs from 'dayjs';
25
  import { humanId } from 'human-id';
26
  import { lowerFirst } from 'lodash';
27
  import trim from 'lodash/trim';
 
448
  return handleRun;
449
  };
450
 
451
+ export const useReplaceIdWithName = () => {
452
  const getNode = useGraphStore((state) => state.getNode);
453
 
454
+ const replaceIdWithName = useCallback(
455
+ (id?: string) => {
456
+ return getNode(id)?.data.name;
457
+ },
458
+ [getNode],
459
+ );
460
+
461
+ return replaceIdWithName;
462
+ };
463
+
464
+ export const useReplaceIdWithText = (output: unknown) => {
465
+ const getNameById = useReplaceIdWithName();
466
 
467
  return {
468
  replacedOutput: replaceIdWithText(output, getNameById),
 
558
  );
559
 
560
  useEffect(() => {
561
+ console.info('xxx');
562
  nodes.forEach((node) => {
563
  const currentNode = getNode(node.id);
564
  const form = currentNode?.data.form ?? {};
 
680
  };
681
  }, [onPasteCapture]);
682
  };
683
+
684
+ export const useWatchAgentChange = () => {
685
+ const [time, setTime] = useState<string>();
686
+ const nodes = useGraphStore((state) => state.nodes);
687
+ const edges = useGraphStore((state) => state.edges);
688
+ const { saveGraph } = useSaveGraph();
689
+ const { data: flowDetail } = useFetchFlow();
690
+
691
+ const setSaveTime = useCallback((updateTime: number) => {
692
+ setTime(dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss'));
693
+ }, []);
694
+
695
+ useEffect(() => {
696
+ setSaveTime(flowDetail?.update_time);
697
+ }, [flowDetail, setSaveTime]);
698
+
699
+ const saveAgent = useCallback(async () => {
700
+ const ret = await saveGraph();
701
+ setSaveTime(ret.data.update_time);
702
+ }, [saveGraph, setSaveTime]);
703
+
704
+ useDebounceEffect(
705
+ () => {
706
+ saveAgent();
707
+ },
708
+ [nodes, edges],
709
+ {
710
+ wait: 1000 * 20,
711
+ },
712
+ );
713
+
714
+ return time;
715
+ };
web/src/pages/flow/store.ts CHANGED
@@ -24,6 +24,7 @@ import { immer } from 'zustand/middleware/immer';
24
  import { Operator, SwitchElseTo } from './constant';
25
  import { NodeData } from './interface';
26
  import {
 
27
  generateNodeNamesWithIncreasingIndex,
28
  getNodeDragHandle,
29
  getOperatorIndex,
@@ -242,7 +243,10 @@ const useGraphStore = create<RFState>()(
242
 
243
  addNode({
244
  ...(node || {}),
245
- data: { ...(node?.data ?? {}), name: generateNodeName(name) },
 
 
 
246
  selected: false,
247
  dragging: false,
248
  id: `${node?.data?.label}:${humanId()}`,
 
24
  import { Operator, SwitchElseTo } from './constant';
25
  import { NodeData } from './interface';
26
  import {
27
+ duplicateNodeForm,
28
  generateNodeNamesWithIncreasingIndex,
29
  getNodeDragHandle,
30
  getOperatorIndex,
 
243
 
244
  addNode({
245
  ...(node || {}),
246
+ data: {
247
+ ...duplicateNodeForm(node?.data),
248
+ name: generateNodeName(name),
249
+ },
250
  selected: false,
251
  dragging: false,
252
  id: `${node?.data?.label}:${humanId()}`,
web/src/pages/flow/utils.ts CHANGED
@@ -289,3 +289,31 @@ export const generateNodeNamesWithIncreasingIndex = (
289
 
290
  return `${name}_${index}`;
291
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
  return `${name}_${index}`;
291
  };
292
+
293
+ export const duplicateNodeForm = (nodeData?: NodeData) => {
294
+ const form: Record<string, any> = { ...(nodeData?.form ?? {}) };
295
+
296
+ // Delete the downstream node corresponding to the to field of the Categorize operator
297
+ if (nodeData?.label === Operator.Categorize) {
298
+ form.category_description = Object.keys(form.category_description).reduce<
299
+ Record<string, Record<string, any>>
300
+ >((pre, cur) => {
301
+ pre[cur] = {
302
+ ...form.category_description[cur],
303
+ to: undefined,
304
+ };
305
+ return pre;
306
+ }, {});
307
+ }
308
+
309
+ // Delete the downstream nodes corresponding to the yes and no fields of the Relevant operator
310
+ if (nodeData?.label === Operator.Relevant) {
311
+ form.yes = undefined;
312
+ form.no = undefined;
313
+ }
314
+
315
+ return {
316
+ ...(nodeData ?? {}),
317
+ form,
318
+ };
319
+ };