feat: add CategorizeHandle #918 (#1282)

### What problem does this PR solve?

feat: add CategorizeHandle #918

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-06-27 09:20:19 +08:00 committed by GitHub
parent e43208a1ca
commit fa5695c250
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 198 additions and 65 deletions

View File

@ -0,0 +1,39 @@
import { Handle, Position } from 'reactflow';
// import { v4 as uuid } from 'uuid';
import styles from './index.less';
const DEFAULT_HANDLE_STYLE = {
width: 6,
height: 6,
bottom: -5,
fontSize: 8,
};
interface IProps {
top: number;
right: number;
text: string;
idx: number;
}
const CategorizeHandle = ({ top, right, text, idx }: IProps) => {
return (
<Handle
type="source"
position={Position.Right}
id={`CategorizeHandle${idx}`}
isConnectable
style={{
...DEFAULT_HANDLE_STYLE,
top: `${top}%`,
right: `${right}%`,
background: 'red',
}}
>
<span className={styles.categorizeAnchorPointText}>{text}</span>
</Handle>
);
};
export default CategorizeHandle;

View File

@ -37,6 +37,12 @@
-3px 0 6px -4px rgba(0, 0, 0, 0.12),
-6px 0 16px 6px rgba(0, 0, 0, 0.05);
}
.categorizeAnchorPointText {
position: absolute;
top: -4px;
left: 8px;
white-space: nowrap;
}
}
.selectedNode {
border: 1px solid rgb(59, 118, 244);

View File

@ -4,12 +4,14 @@ import { Handle, NodeProps, Position } from 'reactflow';
import OperateDropdown from '@/components/operate-dropdown';
import { CopyOutlined } from '@ant-design/icons';
import { Flex, MenuProps, Space } from 'antd';
import get from 'lodash/get';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Operator, operatorMap } from '../../constant';
import { CategorizeAnchorPointPositions, Operator } from '../../constant';
import { NodeData } from '../../interface';
import OperatorIcon from '../../operator-icon';
import useGraphStore from '../../store';
import CategorizeHandle from './categorize-handle';
import styles from './index.less';
export function RagNode({
@ -30,7 +32,8 @@ export function RagNode({
duplicateNodeById(id);
}, [id, duplicateNodeById]);
const description = operatorMap[data.label as Operator].description;
const isCategorize = data.label === Operator.Categorize;
const categoryData = get(data, 'form.category_description') ?? {};
const items: MenuProps['items'] = [
{
@ -57,9 +60,7 @@ export function RagNode({
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
>
{/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
</Handle>
></Handle>
<Handle type="source" position={Position.Top} id="d" isConnectable />
<Handle
type="source"
@ -67,34 +68,32 @@ export function RagNode({
isConnectable={isConnectable}
className={styles.handle}
id="b"
>
{/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
</Handle>
></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">
<Space size={6}>
<OperatorIcon
name={data.label as Operator}
fontSize={16}
></OperatorIcon>
{/* {data.label} */}
<OperateDropdown
iconFontSize={14}
deleteItem={deleteNode}
items={items}
></OperateDropdown>
</Space>
{/* <div className={styles.nodeName}>{id}</div> */}
</Flex>
{/* <div>
<Text
ellipsis={{ tooltip: description }}
style={{ width: 130 }}
className={styles.description}
>
{description}
</Text>
</div> */}
<section className={styles.bottomBox}>
<div className={styles.nodeName}>{id}</div>
</section>

View File

@ -1,58 +1,78 @@
import { CloseOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Typography } from 'antd';
import { useBuildCategorizeToOptions } from './hooks';
import { useBuildCategorizeToOptions, useHandleToSelectChange } from './hooks';
const DynamicCategorize = () => {
interface IProps {
nodeId?: string;
}
const DynamicCategorize = ({ nodeId }: IProps) => {
const form = Form.useFormInstance();
const options = useBuildCategorizeToOptions();
const { handleSelectChange } = useHandleToSelectChange(
options.map((x) => x.value),
nodeId,
);
return (
<>
<Form.List name="items">
{(fields, { add, remove }) => (
<div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}>
{fields.map((field) => (
<Card
size="small"
key={field.key}
extra={
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
}
>
<Form.Item
label="name"
name={[field.name, 'name']}
initialValue={`Categorize ${field.name + 1}`}
rules={[
{ required: true, message: 'Please input your name!' },
]}
{(fields, { add, remove }) => {
const handleAdd = () => {
const idx = fields.length;
add({ name: `Categorize ${idx + 1}` });
};
return (
<div
style={{ display: 'flex', rowGap: 10, flexDirection: 'column' }}
>
{fields.map((field) => (
<Card
size="small"
key={field.key}
extra={
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
}
>
<Input />
</Form.Item>
<Form.Item
label="description"
name={[field.name, 'description']}
>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label="examples" name={[field.name, 'examples']}>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label="to" name={[field.name, 'to']}>
<Select options={options} />
</Form.Item>
</Card>
))}
<Form.Item
label="name"
name={[field.name, 'name']}
// initialValue={`Categorize ${field.name + 1}`}
rules={[
{ required: true, message: 'Please input your name!' },
]}
>
<Input />
</Form.Item>
<Form.Item
label="description"
name={[field.name, 'description']}
>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label="examples" name={[field.name, 'examples']}>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label="to" name={[field.name, 'to']}>
<Select
allowClear
options={options}
onChange={handleSelectChange}
/>
</Form.Item>
</Card>
))}
<Button type="dashed" onClick={() => add()} block>
+ Add Item
</Button>
</div>
)}
<Button type="dashed" onClick={handleAdd} block>
+ Add Item
</Button>
</div>
);
}}
</Form.List>
<Form.Item noStyle shouldUpdate>

View File

@ -1,6 +1,6 @@
import get from 'lodash/get';
import omit from 'lodash/omit';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { Operator } from '../constant';
import {
ICategorizeItem,
@ -72,6 +72,7 @@ export const useHandleFormValuesChange = ({
}: IOperatorForm) => {
const handleValuesChange = useCallback(
(changedValues: any, values: any) => {
console.info(changedValues, values);
onValuesChange?.(changedValues, {
...omit(values, 'items'),
category_description: buildCategorizeObjectFromList(values.items),
@ -90,3 +91,38 @@ export const useHandleFormValuesChange = ({
return { handleValuesChange };
};
export const useHandleToSelectChange = (
opstionIds: string[],
nodeId?: string,
) => {
// const [previousTarget, setPreviousTarget] = useState('');
const previousTarget = useRef('');
const { addEdge, deleteEdgeBySourceAndTarget } = useGraphStore(
(state) => state,
);
const handleSelectChange = useCallback(
(value?: string) => {
if (nodeId) {
if (previousTarget.current) {
// delete previous edge
deleteEdgeBySourceAndTarget(nodeId, previousTarget.current);
}
if (value) {
addEdge({
source: nodeId,
target: value,
sourceHandle: 'b',
targetHandle: 'd',
});
} else {
// if the value is empty, delete the edges between the current node and all nodes in the drop-down box.
}
previousTarget.current = value;
}
},
[addEdge, nodeId, deleteEdgeBySourceAndTarget],
);
return { handleSelectChange };
};

View File

@ -32,7 +32,7 @@ const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => {
>
<LLMSelect></LLMSelect>
</Form.Item>
<DynamicCategorize></DynamicCategorize>
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>
</Form>
);
};

View File

@ -82,3 +82,18 @@ export const initialFormValuesMap = {
[Operator.Answer]: {},
[Operator.Categorize]: {},
};
export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 },
{ top: 8, right: 18 },
{ top: 15, right: 10 },
{ top: 24, right: 4 },
{ top: 31, right: 1 },
{ top: 38, right: -2 },
{ top: 62, right: -2 }, //bottom
{ top: 71, right: 1 },
{ top: 79, right: 6 },
{ top: 86, right: 12 },
{ top: 91, right: 20 },
{ top: 98, right: 34 },
];

View File

@ -34,10 +34,12 @@ export type RFState = {
onSelectionChange: OnSelectionChangeFunc;
addNode: (nodes: Node) => void;
getNode: (id: string) => Node | undefined;
addEdge: (connection: Connection) => void;
duplicateNode: (id: string) => void;
deleteEdge: () => void;
deleteEdgeById: (id: string) => void;
deleteNodeById: (id: string) => void;
deleteEdgeBySourceAndTarget: (source: string, target: string) => void;
findNodeByName: (operatorName: Operator) => Node | undefined;
findNodeById: (id: string) => Node | undefined;
};
@ -83,6 +85,14 @@ const useGraphStore = create<RFState>()(
getNode: (id: string) => {
return get().nodes.find((x) => x.id === id);
},
addEdge: (connection: Connection) => {
set({
edges: addEdge(connection, get().edges),
});
},
// addOnlyOneEdgeBetweenTwoNodes: (connection: Connection) => {
// },
duplicateNode: (id: string) => {
const { getNode, addNode } = get();
const node = getNode(id);
@ -114,6 +124,14 @@ const useGraphStore = create<RFState>()(
edges: edges.filter((edge) => edge.id !== id),
});
},
deleteEdgeBySourceAndTarget: (source: string, target: string) => {
const { edges } = get();
set({
edges: edges.filter(
(edge) => edge.target !== target && edge.source !== source,
),
});
},
deleteNodeById: (id: string) => {
const { nodes, edges } = get();
set({