### What problem does this PR solve? feat: save the parameters of the generate operator to the form field of the node #918 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.8.0
| @@ -51,7 +51,7 @@ const LlmSettingItems = ({ prefix, formItemLayout = {} }: IProps) => { | |||
| <Divider></Divider> | |||
| <Form.Item | |||
| label={t('freedom')} | |||
| name="parameters" | |||
| name="parameter" | |||
| tooltip={t('freedomTip')} | |||
| {...formItemLayout} | |||
| initialValue={ModelVariableType.Precise} | |||
| @@ -570,12 +570,15 @@ The above is the content you need to summarize.`, | |||
| retrievalDescription: `This component is for the process of retrieving relevent information from knowledge base. So, knowledgebases should be selected. If there's nothing retrieved, the 'Empty response' will be returned.`, | |||
| generateDescription: `This component is used to call LLM to generate text. Be careful about the prompt setting.`, | |||
| categorizeDescription: `This component is used to categorize text. Please specify the name, description and examples of the category. Every single category leads to different downstream components.`, | |||
| relevantDescription: `This component is used to judge if the retrieved information is relevent to user's question. 'Yes' represents that they're relevant. 'No' represents they're irrelevant.`, | |||
| relevantDescription: `This component is used to judge if the output of upstream is relevent to user's latest question. 'Yes' represents that they're relevant. 'No' represents they're irrelevant.`, | |||
| rewriteQuestionDescription: `This component is used to refine user's quesion. Typically, when a user's original question can't retrieve relevant information from knowledge base, this component help you change the question into a proper one which might be more consistant with the expressions in knowledge base. Only 'Retrieval' can be its downstreams.`, | |||
| messageDescription: | |||
| 'This component is used to send user static information.', | |||
| keywordDescription: | |||
| 'This component is used to send user static information.', | |||
| promptText: `Please summarize the following paragraphs. Be careful with the numbers, do not make things up. Paragraphs as following: | |||
| {input} | |||
| The above is the content you need to summarize.`, | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| @@ -145,13 +145,12 @@ const initialLlmBaseValues = { | |||
| }; | |||
| export const initialGenerateValues = { | |||
| // parameters: ModelVariableType.Precise, | |||
| // temperatureEnabled: true, | |||
| ...initialLlmBaseValues, | |||
| prompt: `Please summarize the following paragraphs. Be careful with the numbers, do not make things up. Paragraphs as following: | |||
| {cluster_content} | |||
| {input} | |||
| The above is the content you need to summarize.`, | |||
| cite: true, | |||
| parameters: [], | |||
| }; | |||
| export const initialRewriteQuestionValues = { | |||
| @@ -1,103 +1,96 @@ | |||
| import { EditableCell, EditableRow } from '@/components/editable-cell'; | |||
| 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 { DeleteOutlined } from '@ant-design/icons'; | |||
| import { Button, Flex, Select, Table, TableProps } from 'antd'; | |||
| import { IGenerateParameter } from '../interface'; | |||
| import { | |||
| useBuildComponentIdSelectOptions, | |||
| useHandleOperateParameters, | |||
| } from './hooks'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| nodeId?: string; | |||
| } | |||
| const components = { | |||
| body: { | |||
| row: EditableRow, | |||
| cell: EditableCell, | |||
| }, | |||
| }; | |||
| 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> | |||
| ))} | |||
| const options = useBuildComponentIdSelectOptions(nodeId); | |||
| const { | |||
| dataSource, | |||
| handleAdd, | |||
| handleRemove, | |||
| handleSave, | |||
| handleComponentIdChange, | |||
| } = useHandleOperateParameters(nodeId!); | |||
| <Button type="dashed" onClick={handleAdd} block> | |||
| + Add Item | |||
| </Button> | |||
| </div> | |||
| ); | |||
| }} | |||
| </Form.List> | |||
| 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={options} | |||
| value={text} | |||
| onChange={handleComponentIdChange(record)} | |||
| /> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| title: t('operation'), | |||
| dataIndex: 'operation', | |||
| width: 20, | |||
| key: 'operation', | |||
| align: 'center', | |||
| render(_, record) { | |||
| return <DeleteOutlined onClick={handleRemove(record.id)} />; | |||
| }, | |||
| }, | |||
| ]; | |||
| <Form.Item noStyle shouldUpdate> | |||
| {() => ( | |||
| <Typography> | |||
| <pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre> | |||
| </Typography> | |||
| )} | |||
| </Form.Item> | |||
| </> | |||
| 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> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,102 @@ | |||
| import get from 'lodash/get'; | |||
| import { useCallback, useMemo } from 'react'; | |||
| import { v4 as uuid } from 'uuid'; | |||
| import { Operator } from '../constant'; | |||
| import { IGenerateParameter } from '../interface'; | |||
| import useGraphStore from '../store'; | |||
| // exclude nodes with branches | |||
| const ExcludedNodes = [Operator.Categorize, Operator.Relevant]; | |||
| export const useBuildComponentIdSelectOptions = (nodeId?: string) => { | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| const options = useMemo(() => { | |||
| return nodes | |||
| .filter( | |||
| (x) => | |||
| x.id !== nodeId && !ExcludedNodes.some((y) => y === x.data.label), | |||
| ) | |||
| .map((x) => ({ label: x.data.name, value: x.id })); | |||
| }, [nodes, nodeId]); | |||
| return options; | |||
| }; | |||
| export const useHandleOperateParameters = (nodeId: string) => { | |||
| const { getNode, updateNodeForm } = useGraphStore((state) => state); | |||
| const node = getNode(nodeId); | |||
| const dataSource: IGenerateParameter[] = useMemo( | |||
| () => get(node, 'data.form.parameters', []), | |||
| [node], | |||
| ); | |||
| // const [x, setDataSource] = useState<IGenerateParameter[]>([]); | |||
| const handleComponentIdChange = useCallback( | |||
| (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, | |||
| }); | |||
| updateNodeForm(nodeId, { parameters: newData }); | |||
| // setDataSource(newData); | |||
| }, | |||
| [updateNodeForm, nodeId, dataSource], | |||
| ); | |||
| const handleRemove = useCallback( | |||
| (id?: string) => () => { | |||
| const newData = dataSource.filter((item) => item.id !== id); | |||
| updateNodeForm(nodeId, { parameters: newData }); | |||
| // setDataSource(newData); | |||
| }, | |||
| [updateNodeForm, nodeId, dataSource], | |||
| ); | |||
| const handleAdd = useCallback(() => { | |||
| // setDataSource((state) => [ | |||
| // ...state, | |||
| // { | |||
| // id: uuid(), | |||
| // key: '', | |||
| // component_id: undefined, | |||
| // }, | |||
| // ]); | |||
| updateNodeForm(nodeId, { | |||
| parameters: [ | |||
| ...dataSource, | |||
| { | |||
| id: uuid(), | |||
| key: '', | |||
| component_id: undefined, | |||
| }, | |||
| ], | |||
| }); | |||
| }, [dataSource, nodeId, updateNodeForm]); | |||
| 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, | |||
| }); | |||
| updateNodeForm(nodeId, { parameters: newData }); | |||
| // setDataSource(newData); | |||
| }; | |||
| return { | |||
| handleAdd, | |||
| handleRemove, | |||
| handleComponentIdChange, | |||
| handleSave, | |||
| dataSource, | |||
| }; | |||
| }; | |||
| @@ -9,7 +9,7 @@ | |||
| :global(.editable-cell-value-wrap) { | |||
| padding: 5px 12px; | |||
| cursor: pointer; | |||
| height: 22px !important; | |||
| height: 30px !important; | |||
| } | |||
| &:hover { | |||
| :global(.editable-cell-value-wrap) { | |||
| @@ -3,9 +3,9 @@ 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'; | |||
| import DynamicParameters from './dynamic-parameters'; | |||
| const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => { | |||
| const GenerateForm = ({ onValuesChange, form, node }: IOperatorForm) => { | |||
| const { t } = useTranslate('flow'); | |||
| useSetLlmSetting(form); | |||
| @@ -29,7 +29,7 @@ const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => { | |||
| <Form.Item | |||
| name={['prompt']} | |||
| label={t('prompt', { keyPrefix: 'knowledgeConfiguration' })} | |||
| initialValue={t('promptText', { keyPrefix: 'knowledgeConfiguration' })} | |||
| initialValue={t('promptText')} | |||
| tooltip={t('promptTip', { keyPrefix: 'knowledgeConfiguration' })} | |||
| rules={[ | |||
| { | |||
| @@ -49,7 +49,7 @@ const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => { | |||
| > | |||
| <Switch /> | |||
| </Form.Item> | |||
| <DynamicParameters></DynamicParameters> | |||
| <DynamicParameters nodeId={node?.id}></DynamicParameters> | |||
| </Form> | |||
| ); | |||
| }; | |||
| @@ -1,135 +0,0 @@ | |||
| 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; | |||
| @@ -189,7 +189,17 @@ const useGraphStore = create<RFState>()( | |||
| set({ | |||
| nodes: get().nodes.map((node) => { | |||
| if (node.id === nodeId) { | |||
| node.data = { ...node.data, form: values }; | |||
| // node.data = { | |||
| // ...node.data, | |||
| // form: { ...node.data.form, ...values }, | |||
| // }; | |||
| return { | |||
| ...node, | |||
| data: { | |||
| ...node.data, | |||
| form: { ...node.data.form, ...values }, | |||
| }, | |||
| } as any; | |||
| } | |||
| return node; | |||
| @@ -20,7 +20,7 @@ export const excludeUnEnabledVariables = ( | |||
| export const removeUselessFieldsFromValues = (values: any, prefix?: string) => { | |||
| const nextValues: any = omit(values, [ | |||
| ...Object.keys(variableEnabledFieldMap), | |||
| 'parameters', | |||
| 'parameter', | |||
| ...excludeUnEnabledVariables(values, prefix), | |||
| ]); | |||