### What problem does this PR solve? feat: add RelevantForm #918 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.8.0
| @@ -558,6 +558,8 @@ The above is the content you need to summarize.`, | |||
| addField: 'Add field', | |||
| loop: 'Loop', | |||
| createFlow: 'Create a workflow', | |||
| yes: 'Yes', | |||
| no: 'No', | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| @@ -23,11 +23,13 @@ import ChatDrawer from '../chat/drawer'; | |||
| import styles from './index.less'; | |||
| import { BeginNode } from './node/begin-node'; | |||
| import { CategorizeNode } from './node/categorize-node'; | |||
| import { RelevantNode } from './node/relevant-node'; | |||
| const nodeTypes = { | |||
| ragNode: RagNode, | |||
| categorizeNode: CategorizeNode, | |||
| beginNode: BeginNode, | |||
| relevantNode: RelevantNode, | |||
| }; | |||
| const edgeTypes = { | |||
| @@ -14,7 +14,7 @@ interface IProps { | |||
| top: number; | |||
| right: number; | |||
| text: string; | |||
| idx: number; | |||
| idx?: number; | |||
| } | |||
| const CategorizeHandle = ({ top, right, text, idx }: IProps) => { | |||
| @@ -30,6 +30,7 @@ const CategorizeHandle = ({ top, right, text, idx }: IProps) => { | |||
| top: `${top}%`, | |||
| right: `${right}%`, | |||
| background: 'red', | |||
| color: 'black', | |||
| }} | |||
| > | |||
| <span className={styles.categorizeAnchorPointText}>{text}</span> | |||
| @@ -1,16 +1,10 @@ | |||
| import { Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import get from 'lodash/get'; | |||
| import pick from 'lodash/pick'; | |||
| import { Handle, NodeProps, Position } from 'reactflow'; | |||
| import { | |||
| CategorizeAnchorPointPositions, | |||
| Operator, | |||
| operatorMap, | |||
| } from '../../constant'; | |||
| import { Operator, operatorMap } from '../../constant'; | |||
| import { NodeData } from '../../interface'; | |||
| import OperatorIcon from '../../operator-icon'; | |||
| import CategorizeHandle from './categorize-handle'; | |||
| import NodeDropdown from './dropdown'; | |||
| import styles from './index.less'; | |||
| @@ -20,8 +14,6 @@ export function RagNode({ | |||
| isConnectable = true, | |||
| selected, | |||
| }: NodeProps<NodeData>) { | |||
| const isCategorize = data.label === Operator.Categorize; | |||
| const categoryData = get(data, 'form.category_description') ?? {}; | |||
| const style = operatorMap[data.label as Operator]; | |||
| return ( | |||
| @@ -47,16 +39,6 @@ export function RagNode({ | |||
| id="b" | |||
| ></Handle> | |||
| <Handle type="source" position={Position.Bottom} id="a" isConnectable /> | |||
| {isCategorize && | |||
| Object.keys(categoryData).map((x, idx) => ( | |||
| <CategorizeHandle | |||
| top={CategorizeAnchorPointPositions[idx].top} | |||
| right={CategorizeAnchorPointPositions[idx].right} | |||
| key={idx} | |||
| text={x} | |||
| idx={idx} | |||
| ></CategorizeHandle> | |||
| ))} | |||
| <Flex vertical align="center" justify={'center'} gap={6}> | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| @@ -0,0 +1,64 @@ | |||
| import { Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import pick from 'lodash/pick'; | |||
| import { Handle, NodeProps, Position } from 'reactflow'; | |||
| import { Operator, operatorMap } from '../../constant'; | |||
| import { NodeData } from '../../interface'; | |||
| import OperatorIcon from '../../operator-icon'; | |||
| import NodeDropdown from './dropdown'; | |||
| import CategorizeHandle from './categorize-handle'; | |||
| import styles from './index.less'; | |||
| export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) { | |||
| const style = operatorMap[data.label as Operator]; | |||
| return ( | |||
| <section | |||
| className={classNames(styles.ragNode, { | |||
| [styles.selectedNode]: selected, | |||
| })} | |||
| style={pick(style, ['backgroundColor', 'width', 'height', 'color'])} | |||
| > | |||
| <Handle | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'a'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Top} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'b'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Bottom} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'c'} | |||
| ></Handle> | |||
| <CategorizeHandle top={20} right={6} text={'yes'}></CategorizeHandle> | |||
| <CategorizeHandle top={80} right={6} text={'no'}></CategorizeHandle> | |||
| <Flex vertical align="center" justify="center"> | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| fontSize={style.iconFontSize} | |||
| ></OperatorIcon> | |||
| <span | |||
| className={styles.type} | |||
| style={{ fontSize: style.fontSize ?? 14 }} | |||
| > | |||
| {data.label} | |||
| </span> | |||
| <NodeDropdown id={id}></NodeDropdown> | |||
| </Flex> | |||
| <section className={styles.bottomBox}> | |||
| <div className={styles.nodeName}>{data.name}</div> | |||
| </section> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -2,8 +2,12 @@ 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 { ICategorizeItem } from '../interface'; | |||
| import { useBuildCategorizeToOptions, useHandleToSelectChange } from './hooks'; | |||
| interface IProps { | |||
| nodeId?: string; | |||
| @@ -12,8 +16,11 @@ interface IProps { | |||
| const DynamicCategorize = ({ nodeId }: IProps) => { | |||
| const updateNodeInternals = useUpdateNodeInternals(); | |||
| const form = Form.useFormInstance(); | |||
| const buildCategorizeToOptions = useBuildCategorizeToOptions(); | |||
| const { handleSelectChange } = useHandleToSelectChange(nodeId); | |||
| const buildCategorizeToOptions = useBuildFormSelectOptions( | |||
| Operator.Categorize, | |||
| nodeId, | |||
| ); | |||
| const { handleSelectChange } = useHandleFormSelectChange(nodeId); | |||
| const { t } = useTranslate('flow'); | |||
| return ( | |||
| @@ -2,7 +2,6 @@ import get from 'lodash/get'; | |||
| import omit from 'lodash/omit'; | |||
| import { useCallback, useEffect } from 'react'; | |||
| import { Edge, Node } from 'reactflow'; | |||
| import { Operator } from '../constant'; | |||
| import { | |||
| ICategorizeItem, | |||
| ICategorizeItemResult, | |||
| @@ -11,28 +10,6 @@ import { | |||
| } from '../interface'; | |||
| import useGraphStore from '../store'; | |||
| // exclude some nodes downstream of the classification node | |||
| const excludedNodes = [Operator.Categorize, Operator.Answer, Operator.Begin]; | |||
| export const useBuildCategorizeToOptions = () => { | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| const buildCategorizeToOptions = useCallback( | |||
| (toList: string[]) => { | |||
| return nodes | |||
| .filter( | |||
| (x) => | |||
| excludedNodes.every((y) => y !== x.data.label) && | |||
| !toList.some((y) => y === x.id), // filter out selected values in other to fields from the current drop-down box options | |||
| ) | |||
| .map((x) => ({ label: x.data.name, value: x.id })); | |||
| }, | |||
| [nodes], | |||
| ); | |||
| return buildCategorizeToOptions; | |||
| }; | |||
| /** | |||
| * convert the following object into a list | |||
| * | |||
| @@ -119,32 +96,3 @@ export const useHandleFormValuesChange = ({ | |||
| return { handleValuesChange }; | |||
| }; | |||
| export const useHandleToSelectChange = (nodeId?: string) => { | |||
| const { addEdge, deleteEdgeBySourceAndSourceHandle } = useGraphStore( | |||
| (state) => state, | |||
| ); | |||
| const handleSelectChange = useCallback( | |||
| (name?: string) => (value?: string) => { | |||
| if (nodeId && name) { | |||
| if (value) { | |||
| addEdge({ | |||
| source: nodeId, | |||
| target: value, | |||
| sourceHandle: name, | |||
| targetHandle: null, | |||
| }); | |||
| } else { | |||
| // clear selected value | |||
| deleteEdgeBySourceAndSourceHandle({ | |||
| source: nodeId, | |||
| sourceHandle: name, | |||
| }); | |||
| } | |||
| } | |||
| }, | |||
| [addEdge, nodeId, deleteEdgeBySourceAndSourceHandle], | |||
| ); | |||
| return { handleSelectChange }; | |||
| }; | |||
| @@ -67,7 +67,12 @@ export const operatorMap = { | |||
| }, | |||
| [Operator.Relevant]: { | |||
| description: 'BranchesOutlined description', | |||
| backgroundColor: 'white', | |||
| backgroundColor: '#9fd94d', | |||
| color: 'white', | |||
| width: 70, | |||
| height: 70, | |||
| fontSize: 12, | |||
| iconFontSize: 16, | |||
| }, | |||
| [Operator.RewriteQuestion]: { | |||
| description: 'RewriteQuestion description', | |||
| @@ -136,6 +141,7 @@ export const initialFormValuesMap = { | |||
| [Operator.Generate]: initialGenerateValues, | |||
| [Operator.Answer]: {}, | |||
| [Operator.Categorize]: {}, | |||
| [Operator.Relevant]: {}, | |||
| }; | |||
| export const CategorizeAnchorPointPositions = [ | |||
| @@ -173,6 +179,6 @@ export const NodeMap = { | |||
| [Operator.Generate]: 'ragNode', | |||
| [Operator.Answer]: 'ragNode', | |||
| [Operator.Message]: 'ragNode', | |||
| [Operator.Relevant]: 'ragNode', | |||
| [Operator.Relevant]: 'relevantNode', | |||
| [Operator.RewriteQuestion]: 'ragNode', | |||
| }; | |||
| @@ -0,0 +1,62 @@ | |||
| import { useCallback } from 'react'; | |||
| import { Operator } from './constant'; | |||
| import useGraphStore from './store'; | |||
| const ExcludedNodesMap = { | |||
| // exclude some nodes downstream of the classification node | |||
| [Operator.Categorize]: [Operator.Categorize, Operator.Answer, Operator.Begin], | |||
| [Operator.Relevant]: [Operator.Begin], | |||
| }; | |||
| export const useBuildFormSelectOptions = ( | |||
| operatorName: Operator, | |||
| selfId?: string, // exclude the current node | |||
| ) => { | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| const buildCategorizeToOptions = useCallback( | |||
| (toList: string[]) => { | |||
| const excludedNodes: Operator[] = ExcludedNodesMap[operatorName] ?? []; | |||
| return nodes | |||
| .filter( | |||
| (x) => | |||
| excludedNodes.every((y) => y !== x.data.label) && | |||
| x.id !== selfId && | |||
| !toList.some((y) => y === x.id), // filter out selected values in other to fields from the current drop-down box options | |||
| ) | |||
| .map((x) => ({ label: x.data.name, value: x.id })); | |||
| }, | |||
| [nodes, operatorName, selfId], | |||
| ); | |||
| return buildCategorizeToOptions; | |||
| }; | |||
| export const useHandleFormSelectChange = (nodeId?: string) => { | |||
| const { addEdge, deleteEdgeBySourceAndSourceHandle } = useGraphStore( | |||
| (state) => state, | |||
| ); | |||
| const handleSelectChange = useCallback( | |||
| (name?: string) => (value?: string) => { | |||
| if (nodeId && name) { | |||
| if (value) { | |||
| addEdge({ | |||
| source: nodeId, | |||
| target: value, | |||
| sourceHandle: name, | |||
| targetHandle: null, | |||
| }); | |||
| } else { | |||
| // clear selected value | |||
| deleteEdgeBySourceAndSourceHandle({ | |||
| source: nodeId, | |||
| sourceHandle: name, | |||
| }); | |||
| } | |||
| } | |||
| }, | |||
| [addEdge, nodeId, deleteEdgeBySourceAndSourceHandle], | |||
| ); | |||
| return { handleSelectChange }; | |||
| }; | |||
| @@ -53,6 +53,11 @@ export interface ICategorizeForm extends IGenerateForm { | |||
| category_description: ICategorizeItemResult; | |||
| } | |||
| export interface IRelevantForm extends IGenerateForm { | |||
| yes: string; | |||
| no: string; | |||
| } | |||
| export type NodeData = { | |||
| label: string; // operator type | |||
| name: string; // operator name | |||
| @@ -0,0 +1,44 @@ | |||
| import { useCallback, useEffect } from 'react'; | |||
| import { Edge } from 'reactflow'; | |||
| import { IOperatorForm } from '../interface'; | |||
| import useGraphStore from '../store'; | |||
| export const useBuildRelevantOptions = () => { | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| const buildRelevantOptions = useCallback( | |||
| (toList: string[]) => { | |||
| return nodes | |||
| .filter( | |||
| (x) => !toList.some((y) => y === x.id), // filter out selected values in other to fields from the current drop-down box options | |||
| ) | |||
| .map((x) => ({ label: x.data.name, value: x.id })); | |||
| }, | |||
| [nodes], | |||
| ); | |||
| return buildRelevantOptions; | |||
| }; | |||
| const getTargetOfEdge = (edges: Edge[], sourceHandle: string) => | |||
| edges.find((x) => x.sourceHandle === sourceHandle)?.target; | |||
| /** | |||
| * monitor changes in the connection and synchronize the target to the yes and no fields of the form | |||
| * similar to the categorize-form's useHandleFormValuesChange method | |||
| * @param param0 | |||
| */ | |||
| export const useWatchConnectionChanges = ({ nodeId, form }: IOperatorForm) => { | |||
| const edges = useGraphStore((state) => state.edges); | |||
| const watchConnectionChanges = useCallback(() => { | |||
| const edgeList = edges.filter((x) => x.source === nodeId); | |||
| const yes = getTargetOfEdge(edgeList, 'yes'); | |||
| const no = getTargetOfEdge(edgeList, 'no'); | |||
| form?.setFieldsValue({ yes, no }); | |||
| }, [edges, nodeId, form]); | |||
| useEffect(() => { | |||
| watchConnectionChanges(); | |||
| }, [watchConnectionChanges]); | |||
| }; | |||
| @@ -1,12 +1,24 @@ | |||
| import LLMSelect from '@/components/llm-select'; | |||
| import { useTranslate } from '@/hooks/commonHooks'; | |||
| import { Form } from 'antd'; | |||
| import { Form, Select } from 'antd'; | |||
| import { Operator } from '../constant'; | |||
| import { | |||
| useBuildFormSelectOptions, | |||
| useHandleFormSelectChange, | |||
| } from '../form-hooks'; | |||
| import { useSetLlmSetting } from '../hooks'; | |||
| import { IOperatorForm } from '../interface'; | |||
| import { useWatchConnectionChanges } from './hooks'; | |||
| const RelevantForm = ({ onValuesChange, form }: IOperatorForm) => { | |||
| const { t } = useTranslate('chat'); | |||
| const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => { | |||
| const { t } = useTranslate('flow'); | |||
| useSetLlmSetting(form); | |||
| const buildRelevantOptions = useBuildFormSelectOptions( | |||
| Operator.Relevant, | |||
| node?.id, | |||
| ); | |||
| useWatchConnectionChanges({ nodeId: node?.id, form }); | |||
| const { handleSelectChange } = useHandleFormSelectChange(node?.id); | |||
| return ( | |||
| <Form | |||
| @@ -26,6 +38,20 @@ const RelevantForm = ({ onValuesChange, form }: IOperatorForm) => { | |||
| > | |||
| <LLMSelect></LLMSelect> | |||
| </Form.Item> | |||
| <Form.Item label={t('yes')} name={'yes'}> | |||
| <Select | |||
| allowClear | |||
| options={buildRelevantOptions([form?.getFieldValue('no')])} | |||
| onChange={handleSelectChange('yes')} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item label={t('no')} name={'no'}> | |||
| <Select | |||
| allowClear | |||
| options={buildRelevantOptions([form?.getFieldValue('yes')])} | |||
| onChange={handleSelectChange('no')} | |||
| /> | |||
| </Form.Item> | |||
| </Form> | |||
| ); | |||
| }; | |||
| @@ -107,9 +107,15 @@ const useGraphStore = create<RFState>()( | |||
| return get().edges.find((x) => x.id === id); | |||
| }, | |||
| deletePreviousEdgeOfClassificationNode: (connection: Connection) => { | |||
| // Delete the edge on the classification node anchor when the anchor is connected to other nodes | |||
| // Delete the edge on the classification node or relevant node anchor when the anchor is connected to other nodes | |||
| const { edges, getOperatorTypeFromId } = get(); | |||
| if (getOperatorTypeFromId(connection.source) === Operator.Categorize) { | |||
| // the node containing the anchor | |||
| const anchoredNodes = [Operator.Categorize, Operator.Relevant]; | |||
| if ( | |||
| anchoredNodes.some( | |||
| (x) => x === getOperatorTypeFromId(connection.source), | |||
| ) | |||
| ) { | |||
| const previousEdge = edges.find( | |||
| (x) => | |||
| x.source === connection.source && | |||