feat: add RelevantForm #918 (#1344)

### What problem does this PR solve?

feat: add RelevantForm #918

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-07-03 10:15:19 +08:00 committed by GitHub
parent 25c4c717cb
commit a7423e3a94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 237 additions and 82 deletions

View File

@ -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',

View File

@ -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 = {

View File

@ -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>

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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 (

View File

@ -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 };
};

View File

@ -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',
};

View File

@ -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 };
};

View File

@ -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

View File

@ -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]);
};

View File

@ -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>
);
};

View File

@ -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 &&