balibabu
commited on
Commit
·
4a1cf8b
1
Parent(s):
64b1da0
feat: add DynamicParameters #918 (#1346)
Browse files### What problem does this PR solve?
feat: add DynamicParameters #918
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- web/src/components/editable-cell.tsx +103 -0
- web/src/locales/en.ts +4 -0
- web/src/pages/flow/form-hooks.ts +1 -0
- web/src/pages/flow/generate-form/dynamic-parameters.tsx +104 -0
- web/src/pages/flow/generate-form/index.less +21 -0
- web/src/pages/flow/generate-form/index.tsx +10 -2
- web/src/pages/flow/generate-form/next-dynamic-parameters.tsx +135 -0
- web/src/pages/flow/interface.ts +6 -0
web/src/components/editable-cell.tsx
ADDED
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Form, FormInstance, Input, InputRef } from 'antd';
|
2 |
+
import React, { useContext, useEffect, useRef, useState } from 'react';
|
3 |
+
|
4 |
+
const EditableContext = React.createContext<FormInstance<any> | null>(null);
|
5 |
+
|
6 |
+
interface EditableRowProps {
|
7 |
+
index: number;
|
8 |
+
}
|
9 |
+
|
10 |
+
interface Item {
|
11 |
+
key: string;
|
12 |
+
name: string;
|
13 |
+
age: string;
|
14 |
+
address: string;
|
15 |
+
}
|
16 |
+
|
17 |
+
export const EditableRow: React.FC<EditableRowProps> = ({
|
18 |
+
index,
|
19 |
+
...props
|
20 |
+
}) => {
|
21 |
+
const [form] = Form.useForm();
|
22 |
+
return (
|
23 |
+
<Form form={form} component={false}>
|
24 |
+
<EditableContext.Provider value={form}>
|
25 |
+
<tr {...props} />
|
26 |
+
</EditableContext.Provider>
|
27 |
+
</Form>
|
28 |
+
);
|
29 |
+
};
|
30 |
+
|
31 |
+
interface EditableCellProps {
|
32 |
+
title: React.ReactNode;
|
33 |
+
editable: boolean;
|
34 |
+
children: React.ReactNode;
|
35 |
+
dataIndex: keyof Item;
|
36 |
+
record: Item;
|
37 |
+
handleSave: (record: Item) => void;
|
38 |
+
}
|
39 |
+
|
40 |
+
export const EditableCell: React.FC<EditableCellProps> = ({
|
41 |
+
title,
|
42 |
+
editable,
|
43 |
+
children,
|
44 |
+
dataIndex,
|
45 |
+
record,
|
46 |
+
handleSave,
|
47 |
+
...restProps
|
48 |
+
}) => {
|
49 |
+
const [editing, setEditing] = useState(false);
|
50 |
+
const inputRef = useRef<InputRef>(null);
|
51 |
+
const form = useContext(EditableContext)!;
|
52 |
+
|
53 |
+
useEffect(() => {
|
54 |
+
if (editing) {
|
55 |
+
inputRef.current!.focus();
|
56 |
+
}
|
57 |
+
}, [editing]);
|
58 |
+
|
59 |
+
const toggleEdit = () => {
|
60 |
+
setEditing(!editing);
|
61 |
+
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
|
62 |
+
};
|
63 |
+
|
64 |
+
const save = async () => {
|
65 |
+
try {
|
66 |
+
const values = await form.validateFields();
|
67 |
+
|
68 |
+
toggleEdit();
|
69 |
+
handleSave({ ...record, ...values });
|
70 |
+
} catch (errInfo) {
|
71 |
+
console.log('Save failed:', errInfo);
|
72 |
+
}
|
73 |
+
};
|
74 |
+
|
75 |
+
let childNode = children;
|
76 |
+
|
77 |
+
if (editable) {
|
78 |
+
childNode = editing ? (
|
79 |
+
<Form.Item
|
80 |
+
style={{ margin: 0 }}
|
81 |
+
name={dataIndex}
|
82 |
+
rules={[
|
83 |
+
{
|
84 |
+
required: true,
|
85 |
+
message: `${title} is required.`,
|
86 |
+
},
|
87 |
+
]}
|
88 |
+
>
|
89 |
+
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
|
90 |
+
</Form.Item>
|
91 |
+
) : (
|
92 |
+
<div
|
93 |
+
className="editable-cell-value-wrap"
|
94 |
+
style={{ paddingRight: 24 }}
|
95 |
+
onClick={toggleEdit}
|
96 |
+
>
|
97 |
+
{children}
|
98 |
+
</div>
|
99 |
+
);
|
100 |
+
}
|
101 |
+
|
102 |
+
return <td {...restProps}>{childNode}</td>;
|
103 |
+
};
|
web/src/locales/en.ts
CHANGED
@@ -560,6 +560,10 @@ The above is the content you need to summarize.`,
|
|
560 |
createFlow: 'Create a workflow',
|
561 |
yes: 'Yes',
|
562 |
no: 'No',
|
|
|
|
|
|
|
|
|
563 |
},
|
564 |
footer: {
|
565 |
profile: 'All rights reserved @ React',
|
|
|
560 |
createFlow: 'Create a workflow',
|
561 |
yes: 'Yes',
|
562 |
no: 'No',
|
563 |
+
key: 'key',
|
564 |
+
componentId: 'component id',
|
565 |
+
add: 'Add',
|
566 |
+
operation: 'operation',
|
567 |
},
|
568 |
footer: {
|
569 |
profile: 'All rights reserved @ React',
|
web/src/pages/flow/form-hooks.ts
CHANGED
@@ -6,6 +6,7 @@ const ExcludedNodesMap = {
|
|
6 |
// exclude some nodes downstream of the classification node
|
7 |
[Operator.Categorize]: [Operator.Categorize, Operator.Answer, Operator.Begin],
|
8 |
[Operator.Relevant]: [Operator.Begin],
|
|
|
9 |
};
|
10 |
|
11 |
export const useBuildFormSelectOptions = (
|
|
|
6 |
// exclude some nodes downstream of the classification node
|
7 |
[Operator.Categorize]: [Operator.Categorize, Operator.Answer, Operator.Begin],
|
8 |
[Operator.Relevant]: [Operator.Begin],
|
9 |
+
[Operator.Generate]: [Operator.Begin],
|
10 |
};
|
11 |
|
12 |
export const useBuildFormSelectOptions = (
|
web/src/pages/flow/generate-form/dynamic-parameters.tsx
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useTranslate } from '@/hooks/commonHooks';
|
2 |
+
import { CloseOutlined } from '@ant-design/icons';
|
3 |
+
import { Button, Card, Form, Input, Select, Typography } from 'antd';
|
4 |
+
import { useUpdateNodeInternals } from 'reactflow';
|
5 |
+
import { Operator } from '../constant';
|
6 |
+
import {
|
7 |
+
useBuildFormSelectOptions,
|
8 |
+
useHandleFormSelectChange,
|
9 |
+
} from '../form-hooks';
|
10 |
+
import { IGenerateParameter } from '../interface';
|
11 |
+
|
12 |
+
interface IProps {
|
13 |
+
nodeId?: string;
|
14 |
+
}
|
15 |
+
|
16 |
+
const DynamicParameters = ({ nodeId }: IProps) => {
|
17 |
+
const updateNodeInternals = useUpdateNodeInternals();
|
18 |
+
const form = Form.useFormInstance();
|
19 |
+
const buildCategorizeToOptions = useBuildFormSelectOptions(
|
20 |
+
Operator.Categorize,
|
21 |
+
nodeId,
|
22 |
+
);
|
23 |
+
const { handleSelectChange } = useHandleFormSelectChange(nodeId);
|
24 |
+
const { t } = useTranslate('flow');
|
25 |
+
|
26 |
+
return (
|
27 |
+
<>
|
28 |
+
<Form.List name="parameters">
|
29 |
+
{(fields, { add, remove }) => {
|
30 |
+
const handleAdd = () => {
|
31 |
+
const idx = fields.length;
|
32 |
+
add({ name: `parameter ${idx + 1}` });
|
33 |
+
if (nodeId) updateNodeInternals(nodeId);
|
34 |
+
};
|
35 |
+
return (
|
36 |
+
<div
|
37 |
+
style={{ display: 'flex', rowGap: 10, flexDirection: 'column' }}
|
38 |
+
>
|
39 |
+
{fields.map((field) => (
|
40 |
+
<Card
|
41 |
+
size="small"
|
42 |
+
key={field.key}
|
43 |
+
extra={
|
44 |
+
<CloseOutlined
|
45 |
+
onClick={() => {
|
46 |
+
remove(field.name);
|
47 |
+
}}
|
48 |
+
/>
|
49 |
+
}
|
50 |
+
>
|
51 |
+
<Form.Item
|
52 |
+
label={t('key')} // TODO: repeatability check
|
53 |
+
name={[field.name, 'key']}
|
54 |
+
rules={[{ required: true, message: t('nameMessage') }]}
|
55 |
+
>
|
56 |
+
<Input />
|
57 |
+
</Form.Item>
|
58 |
+
<Form.Item
|
59 |
+
label={t('componentId')}
|
60 |
+
name={[field.name, 'component_id']}
|
61 |
+
>
|
62 |
+
<Select
|
63 |
+
allowClear
|
64 |
+
options={buildCategorizeToOptions(
|
65 |
+
(form.getFieldValue(['parameters']) ?? [])
|
66 |
+
.map((x: IGenerateParameter) => x.component_id)
|
67 |
+
.filter(
|
68 |
+
(x: string) =>
|
69 |
+
x !==
|
70 |
+
form.getFieldValue([
|
71 |
+
'parameters',
|
72 |
+
field.name,
|
73 |
+
'component_id',
|
74 |
+
]),
|
75 |
+
),
|
76 |
+
)}
|
77 |
+
onChange={handleSelectChange(
|
78 |
+
form.getFieldValue(['parameters', field.name, 'key']),
|
79 |
+
)}
|
80 |
+
/>
|
81 |
+
</Form.Item>
|
82 |
+
</Card>
|
83 |
+
))}
|
84 |
+
|
85 |
+
<Button type="dashed" onClick={handleAdd} block>
|
86 |
+
+ Add Item
|
87 |
+
</Button>
|
88 |
+
</div>
|
89 |
+
);
|
90 |
+
}}
|
91 |
+
</Form.List>
|
92 |
+
|
93 |
+
<Form.Item noStyle shouldUpdate>
|
94 |
+
{() => (
|
95 |
+
<Typography>
|
96 |
+
<pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>
|
97 |
+
</Typography>
|
98 |
+
)}
|
99 |
+
</Form.Item>
|
100 |
+
</>
|
101 |
+
);
|
102 |
+
};
|
103 |
+
|
104 |
+
export default DynamicParameters;
|
web/src/pages/flow/generate-form/index.less
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.variableTable {
|
2 |
+
margin-top: 14px;
|
3 |
+
}
|
4 |
+
.editableRow {
|
5 |
+
:global(.editable-cell) {
|
6 |
+
position: relative;
|
7 |
+
}
|
8 |
+
|
9 |
+
:global(.editable-cell-value-wrap) {
|
10 |
+
padding: 5px 12px;
|
11 |
+
cursor: pointer;
|
12 |
+
height: 22px !important;
|
13 |
+
}
|
14 |
+
&:hover {
|
15 |
+
:global(.editable-cell-value-wrap) {
|
16 |
+
padding: 4px 11px;
|
17 |
+
border: 1px solid #d9d9d9;
|
18 |
+
border-radius: 2px;
|
19 |
+
}
|
20 |
+
}
|
21 |
+
}
|
web/src/pages/flow/generate-form/index.tsx
CHANGED
@@ -1,8 +1,9 @@
|
|
1 |
-
import
|
2 |
import { useTranslate } from '@/hooks/commonHooks';
|
3 |
import { Form, Input, Switch } from 'antd';
|
4 |
import { useSetLlmSetting } from '../hooks';
|
5 |
import { IOperatorForm } from '../interface';
|
|
|
6 |
|
7 |
const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => {
|
8 |
const { t } = useTranslate('flow');
|
@@ -18,7 +19,13 @@ const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => {
|
|
18 |
form={form}
|
19 |
onValuesChange={onValuesChange}
|
20 |
>
|
21 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
<Form.Item
|
23 |
name={['prompt']}
|
24 |
label={t('prompt', { keyPrefix: 'knowledgeConfiguration' })}
|
@@ -42,6 +49,7 @@ const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => {
|
|
42 |
>
|
43 |
<Switch />
|
44 |
</Form.Item>
|
|
|
45 |
</Form>
|
46 |
);
|
47 |
};
|
|
|
1 |
+
import LLMSelect from '@/components/llm-select';
|
2 |
import { useTranslate } from '@/hooks/commonHooks';
|
3 |
import { Form, Input, Switch } from 'antd';
|
4 |
import { useSetLlmSetting } from '../hooks';
|
5 |
import { IOperatorForm } from '../interface';
|
6 |
+
import DynamicParameters from './next-dynamic-parameters';
|
7 |
|
8 |
const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => {
|
9 |
const { t } = useTranslate('flow');
|
|
|
19 |
form={form}
|
20 |
onValuesChange={onValuesChange}
|
21 |
>
|
22 |
+
<Form.Item
|
23 |
+
name={'llm_id'}
|
24 |
+
label={t('model', { keyPrefix: 'chat' })}
|
25 |
+
tooltip={t('modelTip', { keyPrefix: 'chat' })}
|
26 |
+
>
|
27 |
+
<LLMSelect></LLMSelect>
|
28 |
+
</Form.Item>
|
29 |
<Form.Item
|
30 |
name={['prompt']}
|
31 |
label={t('prompt', { keyPrefix: 'knowledgeConfiguration' })}
|
|
|
49 |
>
|
50 |
<Switch />
|
51 |
</Form.Item>
|
52 |
+
<DynamicParameters></DynamicParameters>
|
53 |
</Form>
|
54 |
);
|
55 |
};
|
web/src/pages/flow/generate-form/next-dynamic-parameters.tsx
ADDED
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { EditableCell, EditableRow } from '@/components/editable-cell';
|
2 |
+
import { useTranslate } from '@/hooks/commonHooks';
|
3 |
+
import { DeleteOutlined } from '@ant-design/icons';
|
4 |
+
import { Button, Flex, Select, Table, TableProps } from 'antd';
|
5 |
+
import { useEffect, useState } from 'react';
|
6 |
+
import { v4 as uuid } from 'uuid';
|
7 |
+
import { IGenerateParameter } from '../interface';
|
8 |
+
|
9 |
+
import { Operator } from '../constant';
|
10 |
+
import { useBuildFormSelectOptions } from '../form-hooks';
|
11 |
+
import styles from './index.less';
|
12 |
+
|
13 |
+
interface IProps {
|
14 |
+
nodeId?: string;
|
15 |
+
}
|
16 |
+
|
17 |
+
const components = {
|
18 |
+
body: {
|
19 |
+
row: EditableRow,
|
20 |
+
cell: EditableCell,
|
21 |
+
},
|
22 |
+
};
|
23 |
+
|
24 |
+
const DynamicParameters = ({ nodeId }: IProps) => {
|
25 |
+
const [dataSource, setDataSource] = useState<IGenerateParameter[]>([]);
|
26 |
+
const { t } = useTranslate('flow');
|
27 |
+
|
28 |
+
const buildCategorizeToOptions = useBuildFormSelectOptions(
|
29 |
+
Operator.Generate,
|
30 |
+
nodeId,
|
31 |
+
);
|
32 |
+
|
33 |
+
const handleRemove = (id?: string) => () => {
|
34 |
+
const newData = dataSource.filter((item) => item.id !== id);
|
35 |
+
setDataSource(newData);
|
36 |
+
};
|
37 |
+
|
38 |
+
const handleAdd = () => {
|
39 |
+
setDataSource((state) => [
|
40 |
+
...state,
|
41 |
+
{
|
42 |
+
id: uuid(),
|
43 |
+
key: '',
|
44 |
+
component_id: undefined,
|
45 |
+
},
|
46 |
+
]);
|
47 |
+
};
|
48 |
+
|
49 |
+
const handleSave = (row: IGenerateParameter) => {
|
50 |
+
const newData = [...dataSource];
|
51 |
+
const index = newData.findIndex((item) => row.id === item.id);
|
52 |
+
const item = newData[index];
|
53 |
+
newData.splice(index, 1, {
|
54 |
+
...item,
|
55 |
+
...row,
|
56 |
+
});
|
57 |
+
setDataSource(newData);
|
58 |
+
};
|
59 |
+
|
60 |
+
useEffect(() => {}, [dataSource]);
|
61 |
+
|
62 |
+
const handleOptionalChange = (row: IGenerateParameter) => (value: string) => {
|
63 |
+
const newData = [...dataSource];
|
64 |
+
const index = newData.findIndex((item) => row.id === item.id);
|
65 |
+
const item = newData[index];
|
66 |
+
newData.splice(index, 1, {
|
67 |
+
...item,
|
68 |
+
component_id: value,
|
69 |
+
});
|
70 |
+
setDataSource(newData);
|
71 |
+
};
|
72 |
+
|
73 |
+
const columns: TableProps<IGenerateParameter>['columns'] = [
|
74 |
+
{
|
75 |
+
title: t('key'),
|
76 |
+
dataIndex: 'key',
|
77 |
+
key: 'key',
|
78 |
+
onCell: (record: IGenerateParameter) => ({
|
79 |
+
record,
|
80 |
+
editable: true,
|
81 |
+
dataIndex: 'key',
|
82 |
+
title: 'key',
|
83 |
+
handleSave,
|
84 |
+
}),
|
85 |
+
},
|
86 |
+
{
|
87 |
+
title: t('componentId'),
|
88 |
+
dataIndex: 'component_id',
|
89 |
+
key: 'component_id',
|
90 |
+
align: 'center',
|
91 |
+
render(text, record) {
|
92 |
+
return (
|
93 |
+
<Select
|
94 |
+
style={{ width: '100%' }}
|
95 |
+
allowClear
|
96 |
+
options={buildCategorizeToOptions([])}
|
97 |
+
// onChange={handleSelectChange(
|
98 |
+
// form.getFieldValue(['parameters', field.name, 'key']),
|
99 |
+
// )}
|
100 |
+
/>
|
101 |
+
);
|
102 |
+
},
|
103 |
+
},
|
104 |
+
{
|
105 |
+
title: t('operation'),
|
106 |
+
dataIndex: 'operation',
|
107 |
+
width: 20,
|
108 |
+
key: 'operation',
|
109 |
+
align: 'center',
|
110 |
+
render(_, record) {
|
111 |
+
return <DeleteOutlined onClick={handleRemove(record.id)} />;
|
112 |
+
},
|
113 |
+
},
|
114 |
+
];
|
115 |
+
|
116 |
+
return (
|
117 |
+
<section>
|
118 |
+
<Flex justify="end">
|
119 |
+
<Button size="small" onClick={handleAdd}>
|
120 |
+
{t('add')}
|
121 |
+
</Button>
|
122 |
+
</Flex>
|
123 |
+
<Table
|
124 |
+
dataSource={dataSource}
|
125 |
+
columns={columns}
|
126 |
+
rowKey={'id'}
|
127 |
+
className={styles.variableTable}
|
128 |
+
components={components}
|
129 |
+
rowClassName={() => styles.editableRow}
|
130 |
+
/>
|
131 |
+
</section>
|
132 |
+
);
|
133 |
+
};
|
134 |
+
|
135 |
+
export default DynamicParameters;
|
web/src/pages/flow/interface.ts
CHANGED
@@ -45,6 +45,12 @@ export interface ICategorizeItem {
|
|
45 |
to?: string;
|
46 |
}
|
47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
export type ICategorizeItemResult = Record<
|
49 |
string,
|
50 |
Omit<ICategorizeItem, 'name'>
|
|
|
45 |
to?: string;
|
46 |
}
|
47 |
|
48 |
+
export interface IGenerateParameter {
|
49 |
+
id?: string;
|
50 |
+
key: string;
|
51 |
+
component_id?: string;
|
52 |
+
}
|
53 |
+
|
54 |
export type ICategorizeItemResult = Record<
|
55 |
string,
|
56 |
Omit<ICategorizeItem, 'name'>
|