浏览代码

feat: add DynamicParameters #918 (#1346)

### What problem does this PR solve?

feat: add DynamicParameters #918

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.8.0
balibabu 1年前
父节点
当前提交
f6159ee4d3
没有帐户链接到提交者的电子邮件

+ 103
- 0
web/src/components/editable-cell.tsx 查看文件

@@ -0,0 +1,103 @@
import { Form, FormInstance, Input, InputRef } from 'antd';
import React, { useContext, useEffect, useRef, useState } from 'react';

const EditableContext = React.createContext<FormInstance<any> | null>(null);

interface EditableRowProps {
index: number;
}

interface Item {
key: string;
name: string;
age: string;
address: string;
}

export const EditableRow: React.FC<EditableRowProps> = ({
index,
...props
}) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};

interface EditableCellProps {
title: React.ReactNode;
editable: boolean;
children: React.ReactNode;
dataIndex: keyof Item;
record: Item;
handleSave: (record: Item) => void;
}

export const EditableCell: React.FC<EditableCellProps> = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef<InputRef>(null);
const form = useContext(EditableContext)!;

useEffect(() => {
if (editing) {
inputRef.current!.focus();
}
}, [editing]);

const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
};

const save = async () => {
try {
const values = await form.validateFields();

toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};

let childNode = children;

if (editable) {
childNode = editing ? (
<Form.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[
{
required: true,
message: `${title} is required.`,
},
]}
>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{ paddingRight: 24 }}
onClick={toggleEdit}
>
{children}
</div>
);
}

return <td {...restProps}>{childNode}</td>;
};

+ 4
- 0
web/src/locales/en.ts 查看文件

@@ -560,6 +560,10 @@ The above is the content you need to summarize.`,
createFlow: 'Create a workflow',
yes: 'Yes',
no: 'No',
key: 'key',
componentId: 'component id',
add: 'Add',
operation: 'operation',
},
footer: {
profile: 'All rights reserved @ React',

+ 1
- 0
web/src/pages/flow/form-hooks.ts 查看文件

@@ -6,6 +6,7 @@ const ExcludedNodesMap = {
// exclude some nodes downstream of the classification node
[Operator.Categorize]: [Operator.Categorize, Operator.Answer, Operator.Begin],
[Operator.Relevant]: [Operator.Begin],
[Operator.Generate]: [Operator.Begin],
};

export const useBuildFormSelectOptions = (

+ 104
- 0
web/src/pages/flow/generate-form/dynamic-parameters.tsx 查看文件

@@ -0,0 +1,104 @@
import { useTranslate } from '@/hooks/commonHooks';
import { CloseOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Typography } from 'antd';
import { useUpdateNodeInternals } from 'reactflow';
import { Operator } from '../constant';
import {
useBuildFormSelectOptions,
useHandleFormSelectChange,
} from '../form-hooks';
import { IGenerateParameter } from '../interface';

interface IProps {
nodeId?: string;
}

const DynamicParameters = ({ nodeId }: IProps) => {
const updateNodeInternals = useUpdateNodeInternals();
const form = Form.useFormInstance();
const buildCategorizeToOptions = useBuildFormSelectOptions(
Operator.Categorize,
nodeId,
);
const { handleSelectChange } = useHandleFormSelectChange(nodeId);
const { t } = useTranslate('flow');

return (
<>
<Form.List name="parameters">
{(fields, { add, remove }) => {
const handleAdd = () => {
const idx = fields.length;
add({ name: `parameter ${idx + 1}` });
if (nodeId) updateNodeInternals(nodeId);
};
return (
<div
style={{ display: 'flex', rowGap: 10, flexDirection: 'column' }}
>
{fields.map((field) => (
<Card
size="small"
key={field.key}
extra={
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
}
>
<Form.Item
label={t('key')} // TODO: repeatability check
name={[field.name, 'key']}
rules={[{ required: true, message: t('nameMessage') }]}
>
<Input />
</Form.Item>
<Form.Item
label={t('componentId')}
name={[field.name, 'component_id']}
>
<Select
allowClear
options={buildCategorizeToOptions(
(form.getFieldValue(['parameters']) ?? [])
.map((x: IGenerateParameter) => x.component_id)
.filter(
(x: string) =>
x !==
form.getFieldValue([
'parameters',
field.name,
'component_id',
]),
),
)}
onChange={handleSelectChange(
form.getFieldValue(['parameters', field.name, 'key']),
)}
/>
</Form.Item>
</Card>
))}

<Button type="dashed" onClick={handleAdd} block>
+ Add Item
</Button>
</div>
);
}}
</Form.List>

<Form.Item noStyle shouldUpdate>
{() => (
<Typography>
<pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>
</Typography>
)}
</Form.Item>
</>
);
};

export default DynamicParameters;

+ 21
- 0
web/src/pages/flow/generate-form/index.less 查看文件

@@ -0,0 +1,21 @@
.variableTable {
margin-top: 14px;
}
.editableRow {
:global(.editable-cell) {
position: relative;
}

:global(.editable-cell-value-wrap) {
padding: 5px 12px;
cursor: pointer;
height: 22px !important;
}
&:hover {
:global(.editable-cell-value-wrap) {
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 2px;
}
}
}

+ 10
- 2
web/src/pages/flow/generate-form/index.tsx 查看文件

@@ -1,8 +1,9 @@
import LlmSettingItems from '@/components/llm-setting-items';
import LLMSelect from '@/components/llm-select';
import { useTranslate } from '@/hooks/commonHooks';
import { Form, Input, Switch } from 'antd';
import { useSetLlmSetting } from '../hooks';
import { IOperatorForm } from '../interface';
import DynamicParameters from './next-dynamic-parameters';

const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => {
const { t } = useTranslate('flow');
@@ -18,7 +19,13 @@ const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => {
form={form}
onValuesChange={onValuesChange}
>
<LlmSettingItems></LlmSettingItems>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
>
<LLMSelect></LLMSelect>
</Form.Item>
<Form.Item
name={['prompt']}
label={t('prompt', { keyPrefix: 'knowledgeConfiguration' })}
@@ -42,6 +49,7 @@ const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => {
>
<Switch />
</Form.Item>
<DynamicParameters></DynamicParameters>
</Form>
);
};

+ 135
- 0
web/src/pages/flow/generate-form/next-dynamic-parameters.tsx 查看文件

@@ -0,0 +1,135 @@
import { EditableCell, EditableRow } from '@/components/editable-cell';
import { useTranslate } from '@/hooks/commonHooks';
import { DeleteOutlined } from '@ant-design/icons';
import { Button, Flex, Select, Table, TableProps } from 'antd';
import { useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid';
import { IGenerateParameter } from '../interface';

import { Operator } from '../constant';
import { useBuildFormSelectOptions } from '../form-hooks';
import styles from './index.less';

interface IProps {
nodeId?: string;
}

const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};

const DynamicParameters = ({ nodeId }: IProps) => {
const [dataSource, setDataSource] = useState<IGenerateParameter[]>([]);
const { t } = useTranslate('flow');

const buildCategorizeToOptions = useBuildFormSelectOptions(
Operator.Generate,
nodeId,
);

const handleRemove = (id?: string) => () => {
const newData = dataSource.filter((item) => item.id !== id);
setDataSource(newData);
};

const handleAdd = () => {
setDataSource((state) => [
...state,
{
id: uuid(),
key: '',
component_id: undefined,
},
]);
};

const handleSave = (row: IGenerateParameter) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.id === item.id);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row,
});
setDataSource(newData);
};

useEffect(() => {}, [dataSource]);

const handleOptionalChange = (row: IGenerateParameter) => (value: string) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.id === item.id);
const item = newData[index];
newData.splice(index, 1, {
...item,
component_id: value,
});
setDataSource(newData);
};

const columns: TableProps<IGenerateParameter>['columns'] = [
{
title: t('key'),
dataIndex: 'key',
key: 'key',
onCell: (record: IGenerateParameter) => ({
record,
editable: true,
dataIndex: 'key',
title: 'key',
handleSave,
}),
},
{
title: t('componentId'),
dataIndex: 'component_id',
key: 'component_id',
align: 'center',
render(text, record) {
return (
<Select
style={{ width: '100%' }}
allowClear
options={buildCategorizeToOptions([])}
// onChange={handleSelectChange(
// form.getFieldValue(['parameters', field.name, 'key']),
// )}
/>
);
},
},
{
title: t('operation'),
dataIndex: 'operation',
width: 20,
key: 'operation',
align: 'center',
render(_, record) {
return <DeleteOutlined onClick={handleRemove(record.id)} />;
},
},
];

return (
<section>
<Flex justify="end">
<Button size="small" onClick={handleAdd}>
{t('add')}
</Button>
</Flex>
<Table
dataSource={dataSource}
columns={columns}
rowKey={'id'}
className={styles.variableTable}
components={components}
rowClassName={() => styles.editableRow}
/>
</section>
);
};

export default DynamicParameters;

+ 6
- 0
web/src/pages/flow/interface.ts 查看文件

@@ -45,6 +45,12 @@ export interface ICategorizeItem {
to?: string;
}

export interface IGenerateParameter {
id?: string;
key: string;
component_id?: string;
}

export type ICategorizeItemResult = Record<
string,
Omit<ICategorizeItem, 'name'>

正在加载...
取消
保存