浏览代码

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)
tags/v0.8.0
balibabu 1年前
父节点
当前提交
fa5695c250
没有帐户链接到提交者的电子邮件

+ 39
- 0
web/src/pages/flow/canvas/node/categorize-handle.tsx 查看文件

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;

+ 6
- 0
web/src/pages/flow/canvas/node/index.less 查看文件

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

+ 18
- 19
web/src/pages/flow/canvas/node/index.tsx 查看文件

import OperateDropdown from '@/components/operate-dropdown'; import OperateDropdown from '@/components/operate-dropdown';
import { CopyOutlined } from '@ant-design/icons'; import { CopyOutlined } from '@ant-design/icons';
import { Flex, MenuProps, Space } from 'antd'; import { Flex, MenuProps, Space } from 'antd';
import get from 'lodash/get';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Operator, operatorMap } from '../../constant';
import { CategorizeAnchorPointPositions, Operator } from '../../constant';
import { NodeData } from '../../interface'; import { NodeData } from '../../interface';
import OperatorIcon from '../../operator-icon'; import OperatorIcon from '../../operator-icon';
import useGraphStore from '../../store'; import useGraphStore from '../../store';
import CategorizeHandle from './categorize-handle';
import styles from './index.less'; import styles from './index.less';


export function RagNode({ export function RagNode({
duplicateNodeById(id); duplicateNodeById(id);
}, [id, duplicateNodeById]); }, [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'] = [ const items: MenuProps['items'] = [
{ {
position={Position.Left} position={Position.Left}
isConnectable={isConnectable} isConnectable={isConnectable}
className={styles.handle} className={styles.handle}
>
{/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
</Handle>
></Handle>
<Handle type="source" position={Position.Top} id="d" isConnectable /> <Handle type="source" position={Position.Top} id="d" isConnectable />
<Handle <Handle
type="source" type="source"
isConnectable={isConnectable} isConnectable={isConnectable}
className={styles.handle} className={styles.handle}
id="b" id="b"
>
{/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
</Handle>
></Handle>
<Handle type="source" position={Position.Bottom} id="a" isConnectable /> <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"> <Flex vertical align="center" justify="center">
<Space size={6}> <Space size={6}>
<OperatorIcon <OperatorIcon
name={data.label as Operator} name={data.label as Operator}
fontSize={16} fontSize={16}
></OperatorIcon> ></OperatorIcon>
{/* {data.label} */}
<OperateDropdown <OperateDropdown
iconFontSize={14} iconFontSize={14}
deleteItem={deleteNode} deleteItem={deleteNode}
items={items} items={items}
></OperateDropdown> ></OperateDropdown>
</Space> </Space>
{/* <div className={styles.nodeName}>{id}</div> */}
</Flex> </Flex>
{/* <div>
<Text
ellipsis={{ tooltip: description }}
style={{ width: 130 }}
className={styles.description}
>
{description}
</Text>
</div> */}

<section className={styles.bottomBox}> <section className={styles.bottomBox}>
<div className={styles.nodeName}>{id}</div> <div className={styles.nodeName}>{id}</div>
</section> </section>

+ 64
- 44
web/src/pages/flow/categorize-form/dynamic-categorize.tsx 查看文件

import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Typography } from 'antd'; 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 form = Form.useFormInstance();
const options = useBuildCategorizeToOptions(); const options = useBuildCategorizeToOptions();
const { handleSelectChange } = useHandleToSelectChange(
options.map((x) => x.value),
nodeId,
);


return ( return (
<> <>
<Form.List name="items"> <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.List>


<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>

+ 37
- 1
web/src/pages/flow/categorize-form/hooks.ts 查看文件

import get from 'lodash/get'; import get from 'lodash/get';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { Operator } from '../constant'; import { Operator } from '../constant';
import { import {
ICategorizeItem, ICategorizeItem,
}: IOperatorForm) => { }: IOperatorForm) => {
const handleValuesChange = useCallback( const handleValuesChange = useCallback(
(changedValues: any, values: any) => { (changedValues: any, values: any) => {
console.info(changedValues, values);
onValuesChange?.(changedValues, { onValuesChange?.(changedValues, {
...omit(values, 'items'), ...omit(values, 'items'),
category_description: buildCategorizeObjectFromList(values.items), category_description: buildCategorizeObjectFromList(values.items),


return { handleValuesChange }; 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 };
};

+ 1
- 1
web/src/pages/flow/categorize-form/index.tsx 查看文件

> >
<LLMSelect></LLMSelect> <LLMSelect></LLMSelect>
</Form.Item> </Form.Item>
<DynamicCategorize></DynamicCategorize>
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>
</Form> </Form>
); );
}; };

+ 15
- 0
web/src/pages/flow/constant.tsx 查看文件

[Operator.Answer]: {}, [Operator.Answer]: {},
[Operator.Categorize]: {}, [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 },
];

+ 18
- 0
web/src/pages/flow/store.ts 查看文件

onSelectionChange: OnSelectionChangeFunc; onSelectionChange: OnSelectionChangeFunc;
addNode: (nodes: Node) => void; addNode: (nodes: Node) => void;
getNode: (id: string) => Node | undefined; getNode: (id: string) => Node | undefined;
addEdge: (connection: Connection) => void;
duplicateNode: (id: string) => void; duplicateNode: (id: string) => void;
deleteEdge: () => void; deleteEdge: () => void;
deleteEdgeById: (id: string) => void; deleteEdgeById: (id: string) => void;
deleteNodeById: (id: string) => void; deleteNodeById: (id: string) => void;
deleteEdgeBySourceAndTarget: (source: string, target: string) => void;
findNodeByName: (operatorName: Operator) => Node | undefined; findNodeByName: (operatorName: Operator) => Node | undefined;
findNodeById: (id: string) => Node | undefined; findNodeById: (id: string) => Node | undefined;
}; };
getNode: (id: string) => { getNode: (id: string) => {
return get().nodes.find((x) => x.id === id); return get().nodes.find((x) => x.id === id);
}, },
addEdge: (connection: Connection) => {
set({
edges: addEdge(connection, get().edges),
});
},
// addOnlyOneEdgeBetweenTwoNodes: (connection: Connection) => {

// },
duplicateNode: (id: string) => { duplicateNode: (id: string) => {
const { getNode, addNode } = get(); const { getNode, addNode } = get();
const node = getNode(id); const node = getNode(id);
edges: edges.filter((edge) => edge.id !== id), 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) => { deleteNodeById: (id: string) => {
const { nodes, edges } = get(); const { nodes, edges } = get();
set({ set({

正在加载...
取消
保存