### 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
| @@ -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>; | |||
| }; | |||
| @@ -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', | |||
| @@ -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 = ( | |||
| @@ -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; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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> | |||
| ); | |||
| }; | |||
| @@ -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; | |||
| @@ -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'> | |||