### 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
| 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>; | |||||
| }; |
| createFlow: 'Create a workflow', | createFlow: 'Create a workflow', | ||||
| yes: 'Yes', | yes: 'Yes', | ||||
| no: 'No', | no: 'No', | ||||
| key: 'key', | |||||
| componentId: 'component id', | |||||
| add: 'Add', | |||||
| operation: 'operation', | |||||
| }, | }, | ||||
| footer: { | footer: { | ||||
| profile: 'All rights reserved @ React', | profile: 'All rights reserved @ React', |
| // exclude some nodes downstream of the classification node | // exclude some nodes downstream of the classification node | ||||
| [Operator.Categorize]: [Operator.Categorize, Operator.Answer, Operator.Begin], | [Operator.Categorize]: [Operator.Categorize, Operator.Answer, Operator.Begin], | ||||
| [Operator.Relevant]: [Operator.Begin], | [Operator.Relevant]: [Operator.Begin], | ||||
| [Operator.Generate]: [Operator.Begin], | |||||
| }; | }; | ||||
| export const useBuildFormSelectOptions = ( | export const useBuildFormSelectOptions = ( |
| 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; |
| .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; | |||||
| } | |||||
| } | |||||
| } |
| import LlmSettingItems from '@/components/llm-setting-items'; | |||||
| import LLMSelect from '@/components/llm-select'; | |||||
| import { useTranslate } from '@/hooks/commonHooks'; | import { useTranslate } from '@/hooks/commonHooks'; | ||||
| import { Form, Input, Switch } from 'antd'; | import { Form, Input, Switch } from 'antd'; | ||||
| import { useSetLlmSetting } from '../hooks'; | import { useSetLlmSetting } from '../hooks'; | ||||
| import { IOperatorForm } from '../interface'; | import { IOperatorForm } from '../interface'; | ||||
| import DynamicParameters from './next-dynamic-parameters'; | |||||
| const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => { | const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => { | ||||
| const { t } = useTranslate('flow'); | const { t } = useTranslate('flow'); | ||||
| form={form} | form={form} | ||||
| onValuesChange={onValuesChange} | 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 | <Form.Item | ||||
| name={['prompt']} | name={['prompt']} | ||||
| label={t('prompt', { keyPrefix: 'knowledgeConfiguration' })} | label={t('prompt', { keyPrefix: 'knowledgeConfiguration' })} | ||||
| > | > | ||||
| <Switch /> | <Switch /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <DynamicParameters></DynamicParameters> | |||||
| </Form> | </Form> | ||||
| ); | ); | ||||
| }; | }; |
| 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; |
| to?: string; | to?: string; | ||||
| } | } | ||||
| export interface IGenerateParameter { | |||||
| id?: string; | |||||
| key: string; | |||||
| component_id?: string; | |||||
| } | |||||
| export type ICategorizeItemResult = Record< | export type ICategorizeItemResult = Record< | ||||
| string, | string, | ||||
| Omit<ICategorizeItem, 'name'> | Omit<ICategorizeItem, 'name'> |