balibabu commited on
Commit
f859b0d
·
1 Parent(s): 362b09b

feat: Add RunDrawer #3355 (#3434)

Browse files

### What problem does this PR solve?

feat: Translation test run form #3355
feat: Wrap QueryTable with Collapse #3355
feat: If the required fields are not filled in, the submit button will
be grayed out. #3355
feat: Add RunDrawer #3355

### Type of change


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

web/src/hooks/document-hooks.ts CHANGED
@@ -4,8 +4,9 @@ import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
4
  import i18n from '@/locales/config';
5
  import chatService from '@/services/chat-service';
6
  import kbService from '@/services/knowledge-service';
7
- import { api_host } from '@/utils/api';
8
  import { buildChunkHighlights } from '@/utils/document-util';
 
9
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
10
  import { UploadFile, message } from 'antd';
11
  import { get } from 'lodash';
@@ -442,3 +443,27 @@ export const useUploadAndParseDocument = (uploadMethod: string) => {
442
 
443
  return { data, loading, uploadAndParseDocument: mutateAsync };
444
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import i18n from '@/locales/config';
5
  import chatService from '@/services/chat-service';
6
  import kbService from '@/services/knowledge-service';
7
+ import api, { api_host } from '@/utils/api';
8
  import { buildChunkHighlights } from '@/utils/document-util';
9
+ import { post } from '@/utils/request';
10
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
11
  import { UploadFile, message } from 'antd';
12
  import { get } from 'lodash';
 
443
 
444
  return { data, loading, uploadAndParseDocument: mutateAsync };
445
  };
446
+
447
+ export const useParseDocument = () => {
448
+ const {
449
+ data,
450
+ isPending: loading,
451
+ mutateAsync,
452
+ } = useMutation({
453
+ mutationKey: ['parseDocument'],
454
+ mutationFn: async (url: string) => {
455
+ try {
456
+ const data = await post(api.parse, { url });
457
+ if (data?.code === 0) {
458
+ message.success(i18n.t('message.uploaded'));
459
+ }
460
+ return data;
461
+ } catch (error) {
462
+ console.log('🚀 ~ mutationFn: ~ error:', error);
463
+ message.error('error');
464
+ }
465
+ },
466
+ });
467
+
468
+ return { parseDocument: mutateAsync, data, loading };
469
+ };
web/src/hooks/login-hooks.ts CHANGED
@@ -2,7 +2,9 @@ import { Authorization } from '@/constants/authorization';
2
  import userService from '@/services/user-service';
3
  import authorizationUtil from '@/utils/authorization-util';
4
  import { useMutation } from '@tanstack/react-query';
5
- import { message } from 'antd';
 
 
6
  import { useTranslation } from 'react-i18next';
7
  import { history } from 'umi';
8
 
@@ -95,3 +97,19 @@ export const useLogout = () => {
95
 
96
  return { data, loading, logout: mutateAsync };
97
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import userService from '@/services/user-service';
3
  import authorizationUtil from '@/utils/authorization-util';
4
  import { useMutation } from '@tanstack/react-query';
5
+ import { Form, message } from 'antd';
6
+ import { FormInstance } from 'antd/lib';
7
+ import { useEffect, useState } from 'react';
8
  import { useTranslation } from 'react-i18next';
9
  import { history } from 'umi';
10
 
 
97
 
98
  return { data, loading, logout: mutateAsync };
99
  };
100
+
101
+ export const useHandleSubmittable = (form: FormInstance) => {
102
+ const [submittable, setSubmittable] = useState<boolean>(false);
103
+
104
+ // Watch all values
105
+ const values = Form.useWatch([], form);
106
+
107
+ useEffect(() => {
108
+ form
109
+ .validateFields({ validateOnly: true })
110
+ .then(() => setSubmittable(true))
111
+ .catch(() => setSubmittable(false));
112
+ }, [form, values]);
113
+
114
+ return { submittable };
115
+ };
web/src/locales/en.ts CHANGED
@@ -32,6 +32,7 @@ export default {
32
  s: 'S',
33
  pleaseSelect: 'Please select',
34
  pleaseInput: 'Please input',
 
35
  },
36
  login: {
37
  login: 'Sign in',
@@ -176,7 +177,7 @@ export default {
176
  chunkTokenNumber: 'Chunk token number',
177
  chunkTokenNumberMessage: 'Chunk token number is required',
178
  embeddingModelTip:
179
- "The model that converts chunks into embeddings. It cannot be changed once the knowledge base has chunks. To switch to a different embedding model, You must delete all chunks in the knowledge base.",
180
  permissionsTip:
181
  "If set to 'Team', all team members will be able to manage the knowledge base.",
182
  chunkTokenNumberTip:
@@ -1025,6 +1026,9 @@ The above is the content you need to summarize.`,
1025
  content: 'Content',
1026
  operationResults: 'Operation Results',
1027
  autosaved: 'Autosaved',
 
 
 
1028
  },
1029
  footer: {
1030
  profile: 'All rights reserved @ React',
 
32
  s: 'S',
33
  pleaseSelect: 'Please select',
34
  pleaseInput: 'Please input',
35
+ submit: 'Submit',
36
  },
37
  login: {
38
  login: 'Sign in',
 
177
  chunkTokenNumber: 'Chunk token number',
178
  chunkTokenNumberMessage: 'Chunk token number is required',
179
  embeddingModelTip:
180
+ 'The model that converts chunks into embeddings. It cannot be changed once the knowledge base has chunks. To switch to a different embedding model, You must delete all chunks in the knowledge base.',
181
  permissionsTip:
182
  "If set to 'Team', all team members will be able to manage the knowledge base.",
183
  chunkTokenNumberTip:
 
1026
  content: 'Content',
1027
  operationResults: 'Operation Results',
1028
  autosaved: 'Autosaved',
1029
+ optional: 'Optional',
1030
+ pasteFileLink: 'Paste file link',
1031
+ testRun: 'Test Run',
1032
  },
1033
  footer: {
1034
  profile: 'All rights reserved @ React',
web/src/locales/zh-traditional.ts CHANGED
@@ -32,6 +32,7 @@ export default {
32
  s: '秒',
33
  pleaseSelect: '請選擇',
34
  pleaseInput: '請輸入',
 
35
  },
36
  login: {
37
  login: '登入',
@@ -985,6 +986,9 @@ export default {
985
  content: '內容',
986
  operationResults: '運行結果',
987
  autosaved: '已自動儲存',
 
 
 
988
  },
989
  footer: {
990
  profile: '“保留所有權利 @ react”',
 
32
  s: '秒',
33
  pleaseSelect: '請選擇',
34
  pleaseInput: '請輸入',
35
+ submit: '提交',
36
  },
37
  login: {
38
  login: '登入',
 
986
  content: '內容',
987
  operationResults: '運行結果',
988
  autosaved: '已自動儲存',
989
+ optional: '可選項',
990
+ pasteFileLink: '貼上文件連結',
991
+ testRun: '試運行',
992
  },
993
  footer: {
994
  profile: '“保留所有權利 @ react”',
web/src/locales/zh.ts CHANGED
@@ -32,6 +32,7 @@ export default {
32
  s: '秒',
33
  pleaseSelect: '请选择',
34
  pleaseInput: '请输入',
 
35
  },
36
  login: {
37
  login: '登录',
@@ -1005,6 +1006,9 @@ export default {
1005
  content: '内容',
1006
  operationResults: '运行结果',
1007
  autosaved: '已自动保存',
 
 
 
1008
  },
1009
  footer: {
1010
  profile: 'All rights reserved @ React',
 
32
  s: '秒',
33
  pleaseSelect: '请选择',
34
  pleaseInput: '请输入',
35
+ submit: '提交',
36
  },
37
  login: {
38
  login: '登录',
 
1006
  content: '内容',
1007
  operationResults: '运行结果',
1008
  autosaved: '已自动保存',
1009
+ optional: '可选项',
1010
+ pasteFileLink: '粘贴文件链接',
1011
+ testRun: '试运行',
1012
  },
1013
  footer: {
1014
  profile: 'All rights reserved @ React',
web/src/pages/flow/canvas/index.tsx CHANGED
@@ -1,4 +1,5 @@
1
- import { useCallback } from 'react';
 
2
  import ReactFlow, {
3
  Background,
4
  ConnectionMode,
@@ -8,14 +9,17 @@ import ReactFlow, {
8
  import 'reactflow/dist/style.css';
9
  import ChatDrawer from '../chat/drawer';
10
  import { Operator } from '../constant';
11
- import FlowDrawer from '../flow-drawer';
12
  import {
 
13
  useHandleDrop,
14
  useSelectCanvasData,
15
- useShowDrawer,
16
  useValidateConnection,
17
  useWatchNodeFormDataChange,
18
  } from '../hooks';
 
 
19
  import { ButtonEdge } from './edge';
20
  import styles from './index.less';
21
  import { RagNode } from './node';
@@ -53,11 +57,11 @@ const edgeTypes = {
53
  };
54
 
55
  interface IProps {
56
- chatDrawerVisible: boolean;
57
- hideChatDrawer(): void;
58
  }
59
 
60
- function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
61
  const {
62
  nodes,
63
  edges,
@@ -67,26 +71,65 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
67
  onSelectionChange,
68
  } = useSelectCanvasData();
69
  const isValidConnection = useValidateConnection();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- const { drawerVisible, hideDrawer, showDrawer, clickedNode } =
72
- useShowDrawer();
 
 
 
 
 
 
 
73
 
74
  const onNodeClick: NodeMouseHandler = useCallback(
75
  (e, node) => {
76
  if (node.data.label !== Operator.Note) {
77
- showDrawer(node);
 
78
  }
79
  },
80
- [showDrawer],
81
  );
82
 
83
- const onPaneClick = useCallback(() => {
84
- hideDrawer();
85
- }, [hideDrawer]);
86
 
87
- const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();
88
-
89
- useWatchNodeFormDataChange();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  return (
92
  <div className={styles.canvasWrapper}>
@@ -147,17 +190,26 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
147
  <Background />
148
  <Controls />
149
  </ReactFlow>
150
- <FlowDrawer
151
- node={clickedNode}
152
- visible={drawerVisible}
153
- hideModal={hideDrawer}
154
- ></FlowDrawer>
155
- {chatDrawerVisible && (
 
 
156
  <ChatDrawer
157
- visible={chatDrawerVisible}
158
- hideModal={hideChatDrawer}
159
  ></ChatDrawer>
160
  )}
 
 
 
 
 
 
 
161
  </div>
162
  );
163
  }
 
1
+ import { useSetModalState } from '@/hooks/common-hooks';
2
+ import { useCallback, useEffect } from 'react';
3
  import ReactFlow, {
4
  Background,
5
  ConnectionMode,
 
9
  import 'reactflow/dist/style.css';
10
  import ChatDrawer from '../chat/drawer';
11
  import { Operator } from '../constant';
12
+ import FormDrawer from '../flow-drawer';
13
  import {
14
+ useGetBeginNodeDataQuery,
15
  useHandleDrop,
16
  useSelectCanvasData,
17
+ useShowFormDrawer,
18
  useValidateConnection,
19
  useWatchNodeFormDataChange,
20
  } from '../hooks';
21
+ import { BeginQuery } from '../interface';
22
+ import RunDrawer from '../run-drawer';
23
  import { ButtonEdge } from './edge';
24
  import styles from './index.less';
25
  import { RagNode } from './node';
 
57
  };
58
 
59
  interface IProps {
60
+ drawerVisible: boolean;
61
+ hideDrawer(): void;
62
  }
63
 
64
+ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
65
  const {
66
  nodes,
67
  edges,
 
71
  onSelectionChange,
72
  } = useSelectCanvasData();
73
  const isValidConnection = useValidateConnection();
74
+ const {
75
+ visible: runVisible,
76
+ showModal: showRunModal,
77
+ hideModal: hideRunModal,
78
+ } = useSetModalState();
79
+ const {
80
+ visible: chatVisible,
81
+ showModal: showChatModal,
82
+ hideModal: hideChatModal,
83
+ } = useSetModalState();
84
+
85
+ const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } =
86
+ useShowFormDrawer();
87
+
88
+ const onPaneClick = useCallback(() => {
89
+ hideFormDrawer();
90
+ }, [hideFormDrawer]);
91
 
92
+ const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();
93
+
94
+ useWatchNodeFormDataChange();
95
+
96
+ const hideRunOrChatDrawer = useCallback(() => {
97
+ hideChatModal();
98
+ hideRunModal();
99
+ hideDrawer();
100
+ }, [hideChatModal, hideDrawer, hideRunModal]);
101
 
102
  const onNodeClick: NodeMouseHandler = useCallback(
103
  (e, node) => {
104
  if (node.data.label !== Operator.Note) {
105
+ hideRunOrChatDrawer();
106
+ showFormDrawer(node);
107
  }
108
  },
109
+ [hideRunOrChatDrawer, showFormDrawer],
110
  );
111
 
112
+ const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
 
 
113
 
114
+ useEffect(() => {
115
+ if (drawerVisible) {
116
+ const query: BeginQuery[] = getBeginNodeDataQuery();
117
+ if (query.length > 0) {
118
+ showRunModal();
119
+ hideChatModal();
120
+ } else {
121
+ showChatModal();
122
+ hideRunModal();
123
+ }
124
+ }
125
+ }, [
126
+ hideChatModal,
127
+ hideRunModal,
128
+ showChatModal,
129
+ showRunModal,
130
+ drawerVisible,
131
+ getBeginNodeDataQuery,
132
+ ]);
133
 
134
  return (
135
  <div className={styles.canvasWrapper}>
 
190
  <Background />
191
  <Controls />
192
  </ReactFlow>
193
+ {formDrawerVisible && (
194
+ <FormDrawer
195
+ node={clickedNode}
196
+ visible={formDrawerVisible}
197
+ hideModal={hideFormDrawer}
198
+ ></FormDrawer>
199
+ )}
200
+ {chatVisible && (
201
  <ChatDrawer
202
+ visible={chatVisible}
203
+ hideModal={hideRunOrChatDrawer}
204
  ></ChatDrawer>
205
  )}
206
+
207
+ {runVisible && (
208
+ <RunDrawer
209
+ hideModal={hideRunOrChatDrawer}
210
+ showModal={showChatModal}
211
+ ></RunDrawer>
212
+ )}
213
  </div>
214
  );
215
  }
web/src/pages/flow/canvas/node/begin-node.tsx CHANGED
@@ -1,9 +1,15 @@
1
  import { Flex } from 'antd';
2
  import classNames from 'classnames';
 
3
  import { useTranslation } from 'react-i18next';
4
  import { Handle, NodeProps, Position } from 'reactflow';
5
- import { Operator, operatorMap } from '../../constant';
6
- import { NodeData } from '../../interface';
 
 
 
 
 
7
  import OperatorIcon from '../../operator-icon';
8
  import { RightHandleStyle } from './handle-icon';
9
  import styles from './index.less';
@@ -11,15 +17,13 @@ import styles from './index.less';
11
  // TODO: do not allow other nodes to connect to this node
12
  export function BeginNode({ selected, data }: NodeProps<NodeData>) {
13
  const { t } = useTranslation();
 
14
 
15
  return (
16
  <section
17
  className={classNames(styles.ragNode, {
18
  [styles.selectedNode]: selected,
19
  })}
20
- style={{
21
- width: 100,
22
- }}
23
  >
24
  <Handle
25
  type="source"
@@ -29,7 +33,7 @@ export function BeginNode({ selected, data }: NodeProps<NodeData>) {
29
  style={RightHandleStyle}
30
  ></Handle>
31
 
32
- <Flex align="center" justify={'space-around'}>
33
  <OperatorIcon
34
  name={data.label as Operator}
35
  fontSize={24}
@@ -37,6 +41,24 @@ export function BeginNode({ selected, data }: NodeProps<NodeData>) {
37
  ></OperatorIcon>
38
  <div className={styles.nodeTitle}>{t(`flow.begin`)}</div>
39
  </Flex>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  </section>
41
  );
42
  }
 
1
  import { Flex } from 'antd';
2
  import classNames from 'classnames';
3
+ import get from 'lodash/get';
4
  import { useTranslation } from 'react-i18next';
5
  import { Handle, NodeProps, Position } from 'reactflow';
6
+ import {
7
+ BeginQueryType,
8
+ BeginQueryTypeIconMap,
9
+ Operator,
10
+ operatorMap,
11
+ } from '../../constant';
12
+ import { BeginQuery, NodeData } from '../../interface';
13
  import OperatorIcon from '../../operator-icon';
14
  import { RightHandleStyle } from './handle-icon';
15
  import styles from './index.less';
 
17
  // TODO: do not allow other nodes to connect to this node
18
  export function BeginNode({ selected, data }: NodeProps<NodeData>) {
19
  const { t } = useTranslation();
20
+ const query: BeginQuery[] = get(data, 'form.query', []);
21
 
22
  return (
23
  <section
24
  className={classNames(styles.ragNode, {
25
  [styles.selectedNode]: selected,
26
  })}
 
 
 
27
  >
28
  <Handle
29
  type="source"
 
33
  style={RightHandleStyle}
34
  ></Handle>
35
 
36
+ <Flex align="center" justify={'center'} gap={10}>
37
  <OperatorIcon
38
  name={data.label as Operator}
39
  fontSize={24}
 
41
  ></OperatorIcon>
42
  <div className={styles.nodeTitle}>{t(`flow.begin`)}</div>
43
  </Flex>
44
+ <Flex gap={8} vertical className={styles.generateParameters}>
45
+ {query.map((x, idx) => {
46
+ const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType];
47
+ return (
48
+ <Flex
49
+ key={idx}
50
+ align="center"
51
+ gap={6}
52
+ className={styles.conditionBlock}
53
+ >
54
+ <Icon className="size-4" />
55
+ <label htmlFor="">{x.key}</label>
56
+ <span className={styles.parameterValue}>{x.name}</span>
57
+ <span className="flex-1">{x.optional ? 'Yes' : 'No'}</span>
58
+ </Flex>
59
+ );
60
+ })}
61
+ </Flex>
62
  </section>
63
  );
64
  }
web/src/pages/flow/constant.tsx CHANGED
@@ -43,6 +43,15 @@ import {
43
  SendOutlined,
44
  } from '@ant-design/icons';
45
  import upperFirst from 'lodash/upperFirst';
 
 
 
 
 
 
 
 
 
46
 
47
  export enum Operator {
48
  Begin = 'Begin',
@@ -2870,12 +2879,12 @@ export enum BeginQueryType {
2870
  Url = 'url',
2871
  }
2872
 
2873
- export const BeginQueryTypeMap = {
2874
- [BeginQueryType.Line]: 'input',
2875
- [BeginQueryType.Paragraph]: 'textarea',
2876
- [BeginQueryType.Options]: 'select',
2877
- [BeginQueryType.File]: 'file',
2878
- [BeginQueryType.Integer]: 'inputnumber',
2879
- [BeginQueryType.Boolean]: 'switch',
2880
- [BeginQueryType.Url]: 'input',
2881
  };
 
43
  SendOutlined,
44
  } from '@ant-design/icons';
45
  import upperFirst from 'lodash/upperFirst';
46
+ import {
47
+ CloudUpload,
48
+ Link2,
49
+ ListOrdered,
50
+ OptionIcon,
51
+ TextCursorInput,
52
+ ToggleLeft,
53
+ WrapText,
54
+ } from 'lucide-react';
55
 
56
  export enum Operator {
57
  Begin = 'Begin',
 
2879
  Url = 'url',
2880
  }
2881
 
2882
+ export const BeginQueryTypeIconMap = {
2883
+ [BeginQueryType.Line]: TextCursorInput,
2884
+ [BeginQueryType.Paragraph]: WrapText,
2885
+ [BeginQueryType.Options]: OptionIcon,
2886
+ [BeginQueryType.File]: CloudUpload,
2887
+ [BeginQueryType.Integer]: ListOrdered,
2888
+ [BeginQueryType.Boolean]: ToggleLeft,
2889
+ [BeginQueryType.Url]: Link2,
2890
  };
web/src/pages/flow/flow-drawer/index.tsx CHANGED
@@ -83,7 +83,7 @@ const FormMap = {
83
 
84
  const EmptyContent = () => <div></div>;
85
 
86
- const FlowDrawer = ({
87
  visible,
88
  hideModal,
89
  node,
@@ -152,4 +152,4 @@ const FlowDrawer = ({
152
  );
153
  };
154
 
155
- export default FlowDrawer;
 
83
 
84
  const EmptyContent = () => <div></div>;
85
 
86
+ const FormDrawer = ({
87
  visible,
88
  hideModal,
89
  node,
 
152
  );
153
  };
154
 
155
+ export default FormDrawer;
web/src/pages/flow/form/begin-form/index.less ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .dynamicInputVariable {
2
+ background-color: #ebe9e9;
3
+ :global(.ant-collapse-content) {
4
+ background-color: #f6f6f6;
5
+ }
6
+ :global(.ant-collapse-content-box) {
7
+ padding: 0 !important;
8
+ }
9
+ margin-bottom: 20px;
10
+ .title {
11
+ font-weight: 600;
12
+ font-size: 16px;
13
+ }
14
+
15
+ .addButton {
16
+ color: rgb(22, 119, 255);
17
+ font-weight: 600;
18
+ }
19
+ }
20
+
21
+ .addButton {
22
+ color: rgb(22, 119, 255);
23
+ font-weight: 600;
24
+ }
web/src/pages/flow/form/begin-form/index.tsx CHANGED
@@ -1,17 +1,20 @@
1
- import { useTranslate } from '@/hooks/common-hooks';
2
  import { Button, Form, Input } from 'antd';
3
  import { useCallback } from 'react';
 
4
  import { BeginQuery, IOperatorForm } from '../../interface';
5
  import { useEditQueryRecord } from './hooks';
6
  import { ModalForm } from './paramater-modal';
7
  import QueryTable from './query-table';
8
 
 
 
9
  type FieldType = {
10
  prologue?: string;
11
  };
12
 
13
  const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
14
- const { t } = useTranslate('chat');
15
  const {
16
  ok,
17
  currentRecord,
@@ -55,9 +58,9 @@ const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
55
  >
56
  <Form.Item<FieldType>
57
  name={'prologue'}
58
- label={t('setAnOpener')}
59
- tooltip={t('setAnOpenerTip')}
60
- initialValue={t('setAnOpenerInitial')}
61
  >
62
  <Input.TextArea autoSize={{ minRows: 5 }} />
63
  </Form.Item>
@@ -65,7 +68,6 @@ const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
65
  <Form.Item name="query" noStyle />
66
 
67
  <Form.Item
68
- label="Query List"
69
  shouldUpdate={(prevValues, curValues) =>
70
  prevValues.query !== curValues.query
71
  }
@@ -86,9 +88,11 @@ const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
86
  htmlType="button"
87
  style={{ margin: '0 8px' }}
88
  onClick={() => showModal()}
 
89
  block
 
90
  >
91
- Add +
92
  </Button>
93
  {visible && (
94
  <ModalForm
 
1
+ import { PlusOutlined } from '@ant-design/icons';
2
  import { Button, Form, Input } from 'antd';
3
  import { useCallback } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
  import { BeginQuery, IOperatorForm } from '../../interface';
6
  import { useEditQueryRecord } from './hooks';
7
  import { ModalForm } from './paramater-modal';
8
  import QueryTable from './query-table';
9
 
10
+ import styles from './index.less';
11
+
12
  type FieldType = {
13
  prologue?: string;
14
  };
15
 
16
  const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
17
+ const { t } = useTranslation();
18
  const {
19
  ok,
20
  currentRecord,
 
58
  >
59
  <Form.Item<FieldType>
60
  name={'prologue'}
61
+ label={t('chat.setAnOpener')}
62
+ tooltip={t('chat.setAnOpenerTip')}
63
+ initialValue={t('chat.setAnOpenerInitial')}
64
  >
65
  <Input.TextArea autoSize={{ minRows: 5 }} />
66
  </Form.Item>
 
68
  <Form.Item name="query" noStyle />
69
 
70
  <Form.Item
 
71
  shouldUpdate={(prevValues, curValues) =>
72
  prevValues.query !== curValues.query
73
  }
 
88
  htmlType="button"
89
  style={{ margin: '0 8px' }}
90
  onClick={() => showModal()}
91
+ icon={<PlusOutlined />}
92
  block
93
+ className={styles.addButton}
94
  >
95
+ {t('flow.addItem')}
96
  </Button>
97
  {visible && (
98
  <ModalForm
web/src/pages/flow/form/begin-form/paramater-modal.tsx CHANGED
@@ -3,7 +3,7 @@ import { IModalProps } from '@/interfaces/common';
3
  import { Form, Input, Modal, Select, Switch } from 'antd';
4
  import { DefaultOptionType } from 'antd/es/select';
5
  import { useEffect, useMemo } from 'react';
6
- import { BeginQueryType } from '../../constant';
7
  import { BeginQuery } from '../../interface';
8
  import BeginDynamicOptions from './begin-dynamic-options';
9
 
@@ -20,10 +20,19 @@ export const ModalForm = ({
20
  const options = useMemo(() => {
21
  return Object.values(BeginQueryType).reduce<DefaultOptionType[]>(
22
  (pre, cur) => {
 
 
23
  return [
24
  ...pre,
25
  {
26
- label: cur,
 
 
 
 
 
 
 
27
  value: cur,
28
  },
29
  ];
 
3
  import { Form, Input, Modal, Select, Switch } from 'antd';
4
  import { DefaultOptionType } from 'antd/es/select';
5
  import { useEffect, useMemo } from 'react';
6
+ import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant';
7
  import { BeginQuery } from '../../interface';
8
  import BeginDynamicOptions from './begin-dynamic-options';
9
 
 
20
  const options = useMemo(() => {
21
  return Object.values(BeginQueryType).reduce<DefaultOptionType[]>(
22
  (pre, cur) => {
23
+ const Icon = BeginQueryTypeIconMap[cur];
24
+
25
  return [
26
  ...pre,
27
  {
28
+ label: (
29
+ <div className="flex items-center gap-2">
30
+ <Icon
31
+ className={`size-${cur === BeginQueryType.Options ? 4 : 5}`}
32
+ ></Icon>
33
+ {cur}
34
+ </div>
35
+ ),
36
  value: cur,
37
  },
38
  ];
web/src/pages/flow/form/begin-form/query-table.tsx CHANGED
@@ -1,8 +1,11 @@
1
  import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
2
  import type { TableProps } from 'antd';
3
- import { Space, Table, Tooltip } from 'antd';
4
  import { BeginQuery } from '../../interface';
5
 
 
 
 
6
  interface IProps {
7
  data: BeginQuery[];
8
  deleteRecord(index: number): void;
@@ -10,6 +13,8 @@ interface IProps {
10
  }
11
 
12
  const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
 
 
13
  const columns: TableProps<BeginQuery>['columns'] = [
14
  {
15
  title: 'Key',
@@ -25,7 +30,7 @@ const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
25
  ),
26
  },
27
  {
28
- title: 'Name',
29
  dataIndex: 'name',
30
  key: 'name',
31
  ellipsis: {
@@ -38,18 +43,18 @@ const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
38
  ),
39
  },
40
  {
41
- title: 'Type',
42
  dataIndex: 'type',
43
  key: 'type',
44
  },
45
  {
46
- title: 'Optional',
47
  dataIndex: 'optional',
48
  key: 'optional',
49
  render: (optional) => (optional ? 'Yes' : 'No'),
50
  },
51
  {
52
- title: 'Action',
53
  key: 'action',
54
  render: (_, record, idx) => (
55
  <Space>
@@ -64,7 +69,23 @@ const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
64
  ];
65
 
66
  return (
67
- <Table<BeginQuery> columns={columns} dataSource={data} pagination={false} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  );
69
  };
70
 
 
1
  import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
2
  import type { TableProps } from 'antd';
3
+ import { Collapse, Space, Table, Tooltip } from 'antd';
4
  import { BeginQuery } from '../../interface';
5
 
6
+ import { useTranslation } from 'react-i18next';
7
+ import styles from './index.less';
8
+
9
  interface IProps {
10
  data: BeginQuery[];
11
  deleteRecord(index: number): void;
 
13
  }
14
 
15
  const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
16
+ const { t } = useTranslation();
17
+
18
  const columns: TableProps<BeginQuery>['columns'] = [
19
  {
20
  title: 'Key',
 
30
  ),
31
  },
32
  {
33
+ title: t('flow.name'),
34
  dataIndex: 'name',
35
  key: 'name',
36
  ellipsis: {
 
43
  ),
44
  },
45
  {
46
+ title: t('flow.type'),
47
  dataIndex: 'type',
48
  key: 'type',
49
  },
50
  {
51
+ title: t('flow.optional'),
52
  dataIndex: 'optional',
53
  key: 'optional',
54
  render: (optional) => (optional ? 'Yes' : 'No'),
55
  },
56
  {
57
+ title: t('common.action'),
58
  key: 'action',
59
  render: (_, record, idx) => (
60
  <Space>
 
69
  ];
70
 
71
  return (
72
+ <Collapse
73
+ defaultActiveKey={['1']}
74
+ className={styles.dynamicInputVariable}
75
+ items={[
76
+ {
77
+ key: '1',
78
+ label: <span className={styles.title}>{t('flow.input')}</span>,
79
+ children: (
80
+ <Table<BeginQuery>
81
+ columns={columns}
82
+ dataSource={data}
83
+ pagination={false}
84
+ />
85
+ ),
86
+ },
87
+ ]}
88
+ />
89
  );
90
  };
91
 
web/src/pages/flow/form/components/dynamic-input-variable.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
2
  import { Button, Collapse, Flex, Form, Input, Select } from 'antd';
3
 
4
- import { useCallback } from 'react';
5
  import { useTranslation } from 'react-i18next';
6
  import { useBuildComponentIdSelectOptions } from '../../hooks';
7
  import styles from './index.less';
@@ -95,9 +95,10 @@ const DynamicVariableForm = ({ nodeId }: IProps) => {
95
  );
96
  };
97
 
98
- const DynamicInputVariable = ({ nodeId }: IProps) => {
99
- const { t } = useTranslation();
100
-
 
101
  return (
102
  <Collapse
103
  className={styles.dynamicInputVariable}
@@ -105,12 +106,21 @@ const DynamicInputVariable = ({ nodeId }: IProps) => {
105
  items={[
106
  {
107
  key: '1',
108
- label: <span className={styles.title}>{t('flow.input')}</span>,
109
- children: <DynamicVariableForm nodeId={nodeId}></DynamicVariableForm>,
110
  },
111
  ]}
112
  />
113
  );
 
 
 
 
 
 
 
 
 
114
  };
115
 
116
  export default DynamicInputVariable;
 
1
  import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
2
  import { Button, Collapse, Flex, Form, Input, Select } from 'antd';
3
 
4
+ import { PropsWithChildren, useCallback } from 'react';
5
  import { useTranslation } from 'react-i18next';
6
  import { useBuildComponentIdSelectOptions } from '../../hooks';
7
  import styles from './index.less';
 
95
  );
96
  };
97
 
98
+ export function FormCollapse({
99
+ children,
100
+ title,
101
+ }: PropsWithChildren<{ title: string }>) {
102
  return (
103
  <Collapse
104
  className={styles.dynamicInputVariable}
 
106
  items={[
107
  {
108
  key: '1',
109
+ label: <span className={styles.title}>{title}</span>,
110
+ children,
111
  },
112
  ]}
113
  />
114
  );
115
+ }
116
+
117
+ const DynamicInputVariable = ({ nodeId }: IProps) => {
118
+ const { t } = useTranslation();
119
+ return (
120
+ <FormCollapse title={t('flow.input')}>
121
+ <DynamicVariableForm nodeId={nodeId}></DynamicVariableForm>
122
+ </FormCollapse>
123
+ );
124
  };
125
 
126
  export default DynamicInputVariable;
web/src/pages/flow/header/index.tsx CHANGED
@@ -3,13 +3,16 @@ import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
3
  import { useFetchFlow } from '@/hooks/flow-hooks';
4
  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 {
 
9
  useSaveGraph,
10
  useSaveGraphBeforeOpeningDebugDrawer,
11
  useWatchAgentChange,
12
  } from '../hooks';
 
13
  import styles from './index.less';
14
 
15
  interface IProps {
@@ -19,7 +22,7 @@ interface IProps {
19
 
20
  const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
21
  const { saveGraph } = useSaveGraph();
22
- const handleRun = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
23
  const { data } = useFetchFlow();
24
  const { t } = useTranslate('flow');
25
  const {
@@ -30,6 +33,16 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
30
  const { visible, hideModal, showModal } = useSetModalState();
31
  const { id } = useParams();
32
  const time = useWatchAgentChange(chatDrawerVisible);
 
 
 
 
 
 
 
 
 
 
33
 
34
  return (
35
  <>
@@ -51,10 +64,10 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
51
  </div>
52
  </Space>
53
  <Space size={'large'}>
54
- <Button onClick={handleRun}>
55
  <b>{t('run')}</b>
56
  </Button>
57
- <Button type="primary" onClick={saveGraph}>
58
  <b>{t('save')}</b>
59
  </Button>
60
  {/* <Button type="primary" onClick={showOverviewModal} disabled>
 
3
  import { useFetchFlow } from '@/hooks/flow-hooks';
4
  import { ArrowLeftOutlined } from '@ant-design/icons';
5
  import { Button, Flex, Space } from 'antd';
6
+ import { useCallback } from 'react';
7
  import { Link, useParams } from 'umi';
8
  import FlowIdModal from '../flow-id-modal';
9
  import {
10
+ useGetBeginNodeDataQuery,
11
  useSaveGraph,
12
  useSaveGraphBeforeOpeningDebugDrawer,
13
  useWatchAgentChange,
14
  } from '../hooks';
15
+ import { BeginQuery } from '../interface';
16
  import styles from './index.less';
17
 
18
  interface IProps {
 
22
 
23
  const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
24
  const { saveGraph } = useSaveGraph();
25
+ const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
26
  const { data } = useFetchFlow();
27
  const { t } = useTranslate('flow');
28
  const {
 
33
  const { visible, hideModal, showModal } = useSetModalState();
34
  const { id } = useParams();
35
  const time = useWatchAgentChange(chatDrawerVisible);
36
+ const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
37
+
38
+ const handleRunAgent = useCallback(() => {
39
+ const query: BeginQuery[] = getBeginNodeDataQuery();
40
+ if (query.length > 0) {
41
+ showChatDrawer();
42
+ } else {
43
+ handleRun();
44
+ }
45
+ }, [getBeginNodeDataQuery, handleRun, showChatDrawer]);
46
 
47
  return (
48
  <>
 
64
  </div>
65
  </Space>
66
  <Space size={'large'}>
67
+ <Button onClick={handleRunAgent}>
68
  <b>{t('run')}</b>
69
  </Button>
70
+ <Button type="primary" onClick={() => saveGraph()}>
71
  <b>{t('save')}</b>
72
  </Button>
73
  {/* <Button type="primary" onClick={showOverviewModal} disabled>
web/src/pages/flow/{hooks.ts → hooks.tsx} RENAMED
@@ -21,6 +21,7 @@ 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 { get, lowerFirst } from 'lodash';
@@ -65,7 +66,12 @@ import {
65
  initialWikipediaValues,
66
  initialYahooFinanceValues,
67
  } from './constant';
68
- import { ICategorizeForm, IRelevantForm, ISwitchForm } from './interface';
 
 
 
 
 
69
  import useGraphStore, { RFState } from './store';
70
  import {
71
  buildDslComponentsByGraph,
@@ -225,49 +231,60 @@ export const useHandleDrop = () => {
225
  return { onDrop, onDragOver, setReactFlowInstance };
226
  };
227
 
228
- export const useShowDrawer = () => {
229
  const {
230
  clickedNodeId: clickNodeId,
231
  setClickedNodeId,
232
  getNode,
233
  } = useGraphStore((state) => state);
234
  const {
235
- visible: drawerVisible,
236
- hideModal: hideDrawer,
237
- showModal: showDrawer,
238
  } = useSetModalState();
239
 
240
  const handleShow = useCallback(
241
  (node: Node) => {
242
  setClickedNodeId(node.id);
243
- showDrawer();
244
  },
245
- [showDrawer, setClickedNodeId],
246
  );
247
 
248
  return {
249
- drawerVisible,
250
- hideDrawer,
251
- showDrawer: handleShow,
252
  clickedNode: getNode(clickNodeId),
253
  };
254
  };
255
 
256
  export const useSaveGraph = () => {
257
  const { data } = useFetchFlow();
258
- const { setFlow } = useSetFlow();
259
  const { id } = useParams();
260
  const { nodes, edges } = useGraphStore((state) => state);
261
- const saveGraph = useCallback(async () => {
262
- const dslComponents = buildDslComponentsByGraph(nodes, edges);
263
- return setFlow({
264
- id,
265
- title: data.title,
266
- dsl: { ...data.dsl, graph: { nodes, edges }, components: dslComponents },
267
- });
268
- }, [nodes, edges, setFlow, id, data]);
 
 
 
 
 
 
 
 
 
 
 
269
 
270
- return { saveGraph };
271
  };
272
 
273
  export const useHandleFormValuesChange = (id?: string) => {
@@ -420,32 +437,46 @@ export const useHandleNodeNameChange = ({
420
  return { name, handleNameBlur, handleNameChange };
421
  };
422
 
 
 
 
 
 
 
 
 
 
 
423
  export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => {
424
  const { id } = useParams();
425
- const { saveGraph } = useSaveGraph();
426
  const { resetFlow } = useResetFlow();
427
  const { refetch } = useFetchFlow();
428
  const { send } = useSendMessageWithSse(api.runCanvas);
429
- const handleRun = useCallback(async () => {
430
- const saveRet = await saveGraph();
431
- if (saveRet?.code === 0) {
432
- // Call the reset api before opening the run drawer each time
433
- const resetRet = await resetFlow();
434
- // After resetting, all previous messages will be cleared.
435
- if (resetRet?.code === 0) {
436
- // fetch prologue
437
- const sendRet = await send({ id });
438
- if (receiveMessageError(sendRet)) {
439
- message.error(sendRet?.data?.message);
440
- } else {
441
- refetch();
442
- show();
 
 
 
443
  }
444
  }
445
- }
446
- }, [saveGraph, resetFlow, id, send, show, refetch]);
 
447
 
448
- return handleRun;
449
  };
450
 
451
  export const useReplaceIdWithName = () => {
@@ -596,8 +627,10 @@ const ExcludedNodes = [
596
 
597
  export const useBuildComponentIdSelectOptions = (nodeId?: string) => {
598
  const nodes = useGraphStore((state) => state.nodes);
 
 
599
 
600
- const options = useMemo(() => {
601
  return nodes
602
  .filter(
603
  (x) =>
@@ -606,17 +639,40 @@ export const useBuildComponentIdSelectOptions = (nodeId?: string) => {
606
  .map((x) => ({ label: x.data.name, value: x.id }));
607
  }, [nodes, nodeId]);
608
 
609
- return options;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
  };
611
 
612
  export const useGetComponentLabelByValue = (nodeId: string) => {
613
  const options = useBuildComponentIdSelectOptions(nodeId);
 
 
 
 
 
 
 
614
 
615
  const getLabel = useCallback(
616
  (val?: string) => {
617
- return options.find((x) => x.value === val)?.label;
618
  },
619
- [options],
620
  );
621
  return getLabel;
622
  };
 
21
  import api from '@/utils/api';
22
  import { useDebounceEffect } from 'ahooks';
23
  import { FormInstance, message } from 'antd';
24
+ import { DefaultOptionType } from 'antd/es/select';
25
  import dayjs from 'dayjs';
26
  import { humanId } from 'human-id';
27
  import { get, lowerFirst } from 'lodash';
 
66
  initialWikipediaValues,
67
  initialYahooFinanceValues,
68
  } from './constant';
69
+ import {
70
+ BeginQuery,
71
+ ICategorizeForm,
72
+ IRelevantForm,
73
+ ISwitchForm,
74
+ } from './interface';
75
  import useGraphStore, { RFState } from './store';
76
  import {
77
  buildDslComponentsByGraph,
 
231
  return { onDrop, onDragOver, setReactFlowInstance };
232
  };
233
 
234
+ export const useShowFormDrawer = () => {
235
  const {
236
  clickedNodeId: clickNodeId,
237
  setClickedNodeId,
238
  getNode,
239
  } = useGraphStore((state) => state);
240
  const {
241
+ visible: formDrawerVisible,
242
+ hideModal: hideFormDrawer,
243
+ showModal: showFormDrawer,
244
  } = useSetModalState();
245
 
246
  const handleShow = useCallback(
247
  (node: Node) => {
248
  setClickedNodeId(node.id);
249
+ showFormDrawer();
250
  },
251
+ [showFormDrawer, setClickedNodeId],
252
  );
253
 
254
  return {
255
+ formDrawerVisible,
256
+ hideFormDrawer,
257
+ showFormDrawer: handleShow,
258
  clickedNode: getNode(clickNodeId),
259
  };
260
  };
261
 
262
  export const useSaveGraph = () => {
263
  const { data } = useFetchFlow();
264
+ const { setFlow, loading } = useSetFlow();
265
  const { id } = useParams();
266
  const { nodes, edges } = useGraphStore((state) => state);
267
+ useEffect(() => {}, [nodes]);
268
+ const saveGraph = useCallback(
269
+ async (currentNodes?: Node[]) => {
270
+ const dslComponents = buildDslComponentsByGraph(
271
+ currentNodes ?? nodes,
272
+ edges,
273
+ );
274
+ return setFlow({
275
+ id,
276
+ title: data.title,
277
+ dsl: {
278
+ ...data.dsl,
279
+ graph: { nodes: currentNodes ?? nodes, edges },
280
+ components: dslComponents,
281
+ },
282
+ });
283
+ },
284
+ [nodes, edges, setFlow, id, data],
285
+ );
286
 
287
+ return { saveGraph, loading };
288
  };
289
 
290
  export const useHandleFormValuesChange = (id?: string) => {
 
437
  return { name, handleNameBlur, handleNameChange };
438
  };
439
 
440
+ export const useGetBeginNodeDataQuery = () => {
441
+ const getNode = useGraphStore((state) => state.getNode);
442
+
443
+ const getBeginNodeDataQuery = useCallback(() => {
444
+ return get(getNode('begin'), 'data.form.query', []);
445
+ }, [getNode]);
446
+
447
+ return getBeginNodeDataQuery;
448
+ };
449
+
450
  export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => {
451
  const { id } = useParams();
452
+ const { saveGraph, loading } = useSaveGraph();
453
  const { resetFlow } = useResetFlow();
454
  const { refetch } = useFetchFlow();
455
  const { send } = useSendMessageWithSse(api.runCanvas);
456
+
457
+ const handleRun = useCallback(
458
+ async (nextNodes?: Node[]) => {
459
+ const saveRet = await saveGraph(nextNodes);
460
+ if (saveRet?.code === 0) {
461
+ // Call the reset api before opening the run drawer each time
462
+ const resetRet = await resetFlow();
463
+ // After resetting, all previous messages will be cleared.
464
+ if (resetRet?.code === 0) {
465
+ // fetch prologue
466
+ const sendRet = await send({ id });
467
+ if (receiveMessageError(sendRet)) {
468
+ message.error(sendRet?.data?.message);
469
+ } else {
470
+ refetch();
471
+ show();
472
+ }
473
  }
474
  }
475
+ },
476
+ [saveGraph, resetFlow, send, id, refetch, show],
477
+ );
478
 
479
+ return { handleRun, loading };
480
  };
481
 
482
  export const useReplaceIdWithName = () => {
 
627
 
628
  export const useBuildComponentIdSelectOptions = (nodeId?: string) => {
629
  const nodes = useGraphStore((state) => state.nodes);
630
+ const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
631
+ const query: BeginQuery[] = getBeginNodeDataQuery();
632
 
633
+ const componentIdOptions = useMemo(() => {
634
  return nodes
635
  .filter(
636
  (x) =>
 
639
  .map((x) => ({ label: x.data.name, value: x.id }));
640
  }, [nodes, nodeId]);
641
 
642
+ const groupedOptions = [
643
+ {
644
+ label: <span>Component id</span>,
645
+ title: 'Component Id',
646
+ options: componentIdOptions,
647
+ },
648
+ {
649
+ label: <span>Begin input</span>,
650
+ title: 'Begin input',
651
+ options: query.map((x) => ({
652
+ label: x.name,
653
+ value: `begin@${x.key}`,
654
+ })),
655
+ },
656
+ ];
657
+
658
+ return groupedOptions;
659
  };
660
 
661
  export const useGetComponentLabelByValue = (nodeId: string) => {
662
  const options = useBuildComponentIdSelectOptions(nodeId);
663
+ const flattenOptions = useMemo(
664
+ () =>
665
+ options.reduce<DefaultOptionType[]>((pre, cur) => {
666
+ return [...pre, ...cur.options];
667
+ }, []),
668
+ [options],
669
+ );
670
 
671
  const getLabel = useCallback(
672
  (val?: string) => {
673
+ return flattenOptions.find((x) => x.value === val)?.label;
674
  },
675
+ [flattenOptions],
676
  );
677
  return getLabel;
678
  };
web/src/pages/flow/index.tsx CHANGED
@@ -31,8 +31,8 @@ function RagFlow() {
31
  ></FlowHeader>
32
  <Content style={{ margin: 0 }}>
33
  <FlowCanvas
34
- chatDrawerVisible={chatDrawerVisible}
35
- hideChatDrawer={hideChatDrawer}
36
  ></FlowCanvas>
37
  </Content>
38
  </Layout>
 
31
  ></FlowHeader>
32
  <Content style={{ margin: 0 }}>
33
  <FlowCanvas
34
+ drawerVisible={chatDrawerVisible}
35
+ hideDrawer={hideChatDrawer}
36
  ></FlowCanvas>
37
  </Content>
38
  </Layout>
web/src/pages/flow/run-drawer/index.less ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .formWrapper {
2
+ :global(.ant-form-item-label) {
3
+ font-weight: 600 !important;
4
+ }
5
+ }
web/src/pages/flow/run-drawer/index.tsx ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Authorization } from '@/constants/authorization';
2
+ import { useSetModalState } from '@/hooks/common-hooks';
3
+ import { useSetSelectedRecord } from '@/hooks/logic-hooks';
4
+ import { useHandleSubmittable } from '@/hooks/login-hooks';
5
+ import { IModalProps } from '@/interfaces/common';
6
+ import api from '@/utils/api';
7
+ import { getAuthorization } from '@/utils/authorization-util';
8
+ import { InboxOutlined } from '@ant-design/icons';
9
+ import {
10
+ Button,
11
+ Drawer,
12
+ Flex,
13
+ Form,
14
+ FormItemProps,
15
+ Input,
16
+ InputNumber,
17
+ Select,
18
+ Switch,
19
+ Upload,
20
+ } from 'antd';
21
+ import { pick } from 'lodash';
22
+ import { Link2, Trash2 } from 'lucide-react';
23
+ import { useCallback } from 'react';
24
+ import { useTranslation } from 'react-i18next';
25
+ import { BeginQueryType } from '../constant';
26
+ import {
27
+ useGetBeginNodeDataQuery,
28
+ useSaveGraphBeforeOpeningDebugDrawer,
29
+ } from '../hooks';
30
+ import { BeginQuery } from '../interface';
31
+ import useGraphStore from '../store';
32
+ import { getDrawerWidth } from '../utils';
33
+ import { PopoverForm } from './popover-form';
34
+
35
+ import styles from './index.less';
36
+
37
+ const RunDrawer = ({
38
+ hideModal,
39
+ showModal: showChatModal,
40
+ }: IModalProps<any>) => {
41
+ const { t } = useTranslation();
42
+ const [form] = Form.useForm();
43
+ const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
44
+ const {
45
+ visible,
46
+ hideModal: hidePopover,
47
+ switchVisible,
48
+ showModal: showPopover,
49
+ } = useSetModalState();
50
+ const { setRecord, currentRecord } = useSetSelectedRecord<number>();
51
+ const { submittable } = useHandleSubmittable(form);
52
+
53
+ const handleShowPopover = useCallback(
54
+ (idx: number) => () => {
55
+ setRecord(idx);
56
+ showPopover();
57
+ },
58
+ [setRecord, showPopover],
59
+ );
60
+
61
+ const handleRemoveUrl = useCallback(
62
+ (key: number, index: number) => () => {
63
+ const list: any[] = form.getFieldValue(key);
64
+
65
+ form.setFieldValue(
66
+ key,
67
+ list.filter((_, idx) => idx !== index),
68
+ );
69
+ },
70
+ [form],
71
+ );
72
+
73
+ const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
74
+ const query: BeginQuery[] = getBeginNodeDataQuery();
75
+
76
+ const normFile = (e: any) => {
77
+ if (Array.isArray(e)) {
78
+ return e;
79
+ }
80
+ return e?.fileList;
81
+ };
82
+
83
+ const renderWidget = useCallback(
84
+ (q: BeginQuery, idx: number) => {
85
+ const props: FormItemProps & { key: number } = {
86
+ key: idx,
87
+ label: q.name,
88
+ name: idx,
89
+ };
90
+ if (q.optional === false) {
91
+ props.rules = [{ required: true }];
92
+ }
93
+
94
+ const urlList: { url: string; result: string }[] =
95
+ form.getFieldValue(idx) || [];
96
+
97
+ const BeginQueryTypeMap = {
98
+ [BeginQueryType.Line]: (
99
+ <Form.Item {...props}>
100
+ <Input></Input>
101
+ </Form.Item>
102
+ ),
103
+ [BeginQueryType.Paragraph]: (
104
+ <Form.Item {...props}>
105
+ <Input.TextArea rows={4}></Input.TextArea>
106
+ </Form.Item>
107
+ ),
108
+ [BeginQueryType.Options]: (
109
+ <Form.Item {...props}>
110
+ <Select
111
+ allowClear
112
+ options={q.options?.map((x) => ({ label: x, value: x })) ?? []}
113
+ ></Select>
114
+ </Form.Item>
115
+ ),
116
+ [BeginQueryType.File]: (
117
+ <Form.Item
118
+ {...props}
119
+ valuePropName="fileList"
120
+ getValueFromEvent={normFile}
121
+ >
122
+ <Upload.Dragger
123
+ name="file"
124
+ action={api.parse}
125
+ multiple
126
+ headers={{ [Authorization]: getAuthorization() }}
127
+ >
128
+ <p className="ant-upload-drag-icon">
129
+ <InboxOutlined />
130
+ </p>
131
+ <p className="ant-upload-text">{t('fileManager.uploadTitle')}</p>
132
+ <p className="ant-upload-hint">
133
+ {t('fileManager.uploadDescription')}
134
+ </p>
135
+ </Upload.Dragger>
136
+ </Form.Item>
137
+ ),
138
+ [BeginQueryType.Integer]: (
139
+ <Form.Item {...props}>
140
+ <InputNumber></InputNumber>
141
+ </Form.Item>
142
+ ),
143
+ [BeginQueryType.Boolean]: (
144
+ <Form.Item valuePropName={'checked'} {...props}>
145
+ <Switch></Switch>
146
+ </Form.Item>
147
+ ),
148
+ [BeginQueryType.Url]: (
149
+ <>
150
+ <Form.Item
151
+ {...pick(props, ['key', 'label', 'rules'])}
152
+ required={!q.optional}
153
+ className={urlList.length > 0 ? 'mb-1' : ''}
154
+ >
155
+ <PopoverForm visible={visible} switchVisible={switchVisible}>
156
+ <Button
157
+ onClick={handleShowPopover(idx)}
158
+ className="text-buttonBlueText"
159
+ >
160
+ {t('flow.pasteFileLink')}
161
+ </Button>
162
+ </PopoverForm>
163
+ </Form.Item>
164
+ <Form.Item name={idx} noStyle {...pick(props, ['rules'])} />
165
+ <Form.Item
166
+ noStyle
167
+ shouldUpdate={(prevValues, curValues) =>
168
+ prevValues[idx] !== curValues[idx]
169
+ }
170
+ >
171
+ {({ getFieldValue }) => {
172
+ const urlInfo: { url: string; result: string }[] =
173
+ getFieldValue(idx) || [];
174
+ return urlInfo.length ? (
175
+ <Flex vertical gap={8} className="mb-3">
176
+ {urlInfo.map((u, index) => (
177
+ <div
178
+ key={index}
179
+ className="flex items-center justify-between gap-2 hover:bg-slate-100 group"
180
+ >
181
+ <Link2 className="size-5"></Link2>
182
+ <span className="flex-1 truncate"> {u.url}</span>
183
+ <Trash2
184
+ className="size-4 invisible group-hover:visible cursor-pointer"
185
+ onClick={handleRemoveUrl(idx, index)}
186
+ />
187
+ </div>
188
+ ))}
189
+ </Flex>
190
+ ) : null;
191
+ }}
192
+ </Form.Item>
193
+ </>
194
+ ),
195
+ };
196
+
197
+ return BeginQueryTypeMap[q.type as BeginQueryType];
198
+ },
199
+ [form, handleRemoveUrl, handleShowPopover, switchVisible, t, visible],
200
+ );
201
+
202
+ const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatModal!);
203
+
204
+ const handleRunAgent = useCallback(
205
+ (nextValues: Record<string, any>) => {
206
+ const currentNodes = updateNodeForm('begin', nextValues, ['query']);
207
+ handleRun(currentNodes);
208
+ hideModal?.();
209
+ },
210
+ [handleRun, hideModal, updateNodeForm],
211
+ );
212
+
213
+ const onOk = useCallback(async () => {
214
+ const values = await form.validateFields();
215
+ const nextValues = Object.entries(values).map(([key, value]) => {
216
+ const item = query[Number(key)];
217
+ let nextValue = value;
218
+ if (Array.isArray(value)) {
219
+ nextValue = ``;
220
+
221
+ value.forEach((x, idx) => {
222
+ if (x?.originFileObj instanceof File) {
223
+ if (idx === 0) {
224
+ nextValue += `${x.name}\n\n${x.response.data}\n\n`;
225
+ } else {
226
+ nextValue += `${x.response.data}\n\n`;
227
+ }
228
+ } else {
229
+ if (idx === 0) {
230
+ nextValue += `${x.url}\n\n${x.result}\n\n`;
231
+ } else {
232
+ nextValue += `${x.result}\n\n`;
233
+ }
234
+ }
235
+ });
236
+ }
237
+ return { ...item, value: nextValue };
238
+ });
239
+ handleRunAgent(nextValues);
240
+ }, [form, handleRunAgent, query]);
241
+
242
+ return (
243
+ <Drawer
244
+ title={t('flow.testRun')}
245
+ placement="right"
246
+ onClose={hideModal}
247
+ open
248
+ getContainer={false}
249
+ width={getDrawerWidth()}
250
+ mask={false}
251
+ >
252
+ <section className={styles.formWrapper}>
253
+ <Form.Provider
254
+ onFormFinish={(name, { values, forms }) => {
255
+ if (name === 'urlForm') {
256
+ const { basicForm } = forms;
257
+ const urlInfo = basicForm.getFieldValue(currentRecord) || [];
258
+ basicForm.setFieldsValue({
259
+ [currentRecord]: [...urlInfo, values],
260
+ });
261
+ hidePopover();
262
+ }
263
+ }}
264
+ >
265
+ <Form
266
+ name="basicForm"
267
+ autoComplete="off"
268
+ layout={'vertical'}
269
+ form={form}
270
+ >
271
+ {query.map((x, idx) => {
272
+ return renderWidget(x, idx);
273
+ })}
274
+ </Form>
275
+ </Form.Provider>
276
+ </section>
277
+ <Button type={'primary'} block onClick={onOk} disabled={!submittable}>
278
+ {t('common.next')}
279
+ </Button>
280
+ </Drawer>
281
+ );
282
+ };
283
+
284
+ export default RunDrawer;
web/src/pages/flow/run-drawer/popover-form.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useParseDocument } from '@/hooks/document-hooks';
2
+ import { useResetFormOnCloseModal } from '@/hooks/logic-hooks';
3
+ import { IModalProps } from '@/interfaces/common';
4
+ import { Button, Form, Input, Popover } from 'antd';
5
+ import { PropsWithChildren } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ const reg =
9
+ /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/;
10
+
11
+ export const PopoverForm = ({
12
+ children,
13
+ visible,
14
+ switchVisible,
15
+ }: PropsWithChildren<IModalProps<any>>) => {
16
+ const [form] = Form.useForm();
17
+ const { parseDocument, loading } = useParseDocument();
18
+ const { t } = useTranslation();
19
+
20
+ useResetFormOnCloseModal({
21
+ form,
22
+ visible,
23
+ });
24
+
25
+ const onOk = async () => {
26
+ const values = await form.validateFields();
27
+ const val = values.url;
28
+
29
+ if (reg.test(val)) {
30
+ const ret = await parseDocument(val);
31
+ if (ret?.data?.code === 0) {
32
+ form.setFieldValue('result', ret?.data?.data);
33
+ form.submit();
34
+ }
35
+ }
36
+ };
37
+
38
+ const content = (
39
+ <Form form={form} name="urlForm">
40
+ <Form.Item
41
+ name="url"
42
+ rules={[{ required: true, type: 'url' }]}
43
+ className="m-0"
44
+ >
45
+ <Input
46
+ onPressEnter={(e) => e.preventDefault()}
47
+ placeholder={t('flow.pasteFileLink')}
48
+ suffix={
49
+ <Button
50
+ type="primary"
51
+ onClick={onOk}
52
+ size={'small'}
53
+ loading={loading}
54
+ >
55
+ {t('common.submit')}
56
+ </Button>
57
+ }
58
+ />
59
+ </Form.Item>
60
+ <Form.Item name={'result'} noStyle />
61
+ </Form>
62
+ );
63
+
64
+ return (
65
+ <Popover
66
+ content={content}
67
+ open={visible}
68
+ trigger={'click'}
69
+ onOpenChange={switchVisible}
70
+ >
71
+ {children}
72
+ </Popover>
73
+ );
74
+ };
web/src/pages/flow/store.ts CHANGED
@@ -47,7 +47,7 @@ export type RFState = {
47
  nodeId: string,
48
  values: any,
49
  path?: (string | number)[],
50
- ) => void;
51
  onSelectionChange: OnSelectionChangeFunc;
52
  addNode: (nodes: Node) => void;
53
  getNode: (id?: string | null) => Node<NodeData> | undefined;
@@ -331,27 +331,30 @@ const useGraphStore = create<RFState>()(
331
  values: any,
332
  path: (string | number)[] = [],
333
  ) => {
334
- set({
335
- nodes: get().nodes.map((node) => {
336
- if (node.id === nodeId) {
337
- let nextForm: Record<string, unknown> = { ...node.data.form };
338
- if (path.length === 0) {
339
- nextForm = Object.assign(nextForm, values);
340
- } else {
341
- lodashSet(nextForm, path, values);
342
- }
343
- return {
344
- ...node,
345
- data: {
346
- ...node.data,
347
- form: nextForm,
348
- },
349
- } as any;
350
  }
 
 
 
 
 
 
 
 
351
 
352
- return node;
353
- }),
354
  });
 
 
 
 
 
355
  },
356
  updateSwitchFormData: (source, sourceHandle, target) => {
357
  const { updateNodeForm } = get();
 
47
  nodeId: string,
48
  values: any,
49
  path?: (string | number)[],
50
+ ) => Node[];
51
  onSelectionChange: OnSelectionChangeFunc;
52
  addNode: (nodes: Node) => void;
53
  getNode: (id?: string | null) => Node<NodeData> | undefined;
 
331
  values: any,
332
  path: (string | number)[] = [],
333
  ) => {
334
+ const nextNodes = get().nodes.map((node) => {
335
+ if (node.id === nodeId) {
336
+ let nextForm: Record<string, unknown> = { ...node.data.form };
337
+ if (path.length === 0) {
338
+ nextForm = Object.assign(nextForm, values);
339
+ } else {
340
+ lodashSet(nextForm, path, values);
 
 
 
 
 
 
 
 
 
341
  }
342
+ return {
343
+ ...node,
344
+ data: {
345
+ ...node.data,
346
+ form: nextForm,
347
+ },
348
+ } as any;
349
+ }
350
 
351
+ return node;
 
352
  });
353
+ set({
354
+ nodes: nextNodes,
355
+ });
356
+
357
+ return nextNodes;
358
  },
359
  updateSwitchFormData: (source, sourceHandle, target) => {
360
  const { updateNodeForm } = get();
web/src/utils/api.ts CHANGED
@@ -62,6 +62,7 @@ export default {
62
  web_crawl: `${api_host}/document/web_crawl`,
63
  document_infos: `${api_host}/document/infos`,
64
  upload_and_parse: `${api_host}/document/upload_and_parse`,
 
65
 
66
  // chat
67
  setDialog: `${api_host}/dialog/set`,
 
62
  web_crawl: `${api_host}/document/web_crawl`,
63
  document_infos: `${api_host}/document/infos`,
64
  upload_and_parse: `${api_host}/document/upload_and_parse`,
65
+ parse: `${api_host}/document/parse`,
66
 
67
  // chat
68
  setDialog: `${api_host}/dialog/set`,
web/src/utils/request.ts CHANGED
@@ -99,8 +99,8 @@ request.interceptors.request.use((url: string, options: any) => {
99
  });
100
 
101
  request.interceptors.response.use(async (response: any, options) => {
102
- if (response?.status === 413) {
103
- message.error(RetcodeMessage[413]);
104
  }
105
 
106
  if (options.responseType === 'blob') {
 
99
  });
100
 
101
  request.interceptors.response.use(async (response: any, options) => {
102
+ if (response?.status === 413 || response?.status === 504) {
103
+ message.error(RetcodeMessage[response?.status as ResultCode]);
104
  }
105
 
106
  if (options.responseType === 'blob') {
web/tailwind.config.js CHANGED
@@ -24,6 +24,7 @@ module.exports = {
24
  ring: 'hsl(var(--ring))',
25
  background: 'var(--background)',
26
  foreground: 'hsl(var(--foreground))',
 
27
  primary: {
28
  DEFAULT: 'hsl(var(--primary))',
29
  foreground: 'hsl(var(--primary-foreground))',
 
24
  ring: 'hsl(var(--ring))',
25
  background: 'var(--background)',
26
  foreground: 'hsl(var(--foreground))',
27
+ buttonBlueText: 'var(--button-blue-text)',
28
  primary: {
29
  DEFAULT: 'hsl(var(--primary))',
30
  foreground: 'hsl(var(--primary-foreground))',
web/tailwind.css CHANGED
@@ -37,6 +37,8 @@
37
 
38
  --background-inverse-standard: rgba(58, 56, 65, 0.15);
39
  --background-inverse-standard-foreground: rgb(92, 81, 81);
 
 
40
  }
41
 
42
  .dark {
 
37
 
38
  --background-inverse-standard: rgba(58, 56, 65, 0.15);
39
  --background-inverse-standard-foreground: rgb(92, 81, 81);
40
+
41
+ --button-blue-text: rgb(22, 119, 255);
42
  }
43
 
44
  .dark {