mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-08-13 00:39:04 +08:00
### 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:
parent
25c4c717cb
commit
a7423e3a94
@ -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}
|
||||
|
64
web/src/pages/flow/canvas/node/relevant-node.tsx
Normal file
64
web/src/pages/flow/canvas/node/relevant-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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',
|
||||
};
|
||||
|
62
web/src/pages/flow/form-hooks.ts
Normal file
62
web/src/pages/flow/form-hooks.ts
Normal 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 };
|
||||
};
|
@ -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
|
||||
|
44
web/src/pages/flow/relevant-form/hooks.ts
Normal file
44
web/src/pages/flow/relevant-form/hooks.ts
Normal 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]);
|
||||
};
|
@ -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 &&
|
||||
|
Loading…
x
Reference in New Issue
Block a user