feat: Add hint for operators, round to square, input variable, readable operator ID. #3056 (#3057)

### What problem does this PR solve?

feat: Add hint for operators, round to square, input variable, readable
operator ID. #3056

### Type of change

- [ ] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):
This commit is contained in:
balibabu
2024-10-28 14:31:19 +08:00
committed by GitHub
parent f93f485696
commit 396feadd4b
56 changed files with 1368 additions and 585 deletions

View File

@ -22,9 +22,15 @@ import styles from './index.less';
import { RagNode } from './node';
import { BeginNode } from './node/begin-node';
import { CategorizeNode } from './node/categorize-node';
import { GenerateNode } from './node/generate-node';
import { KeywordNode } from './node/keyword-node';
import { LogicNode } from './node/logic-node';
import { MessageNode } from './node/message-node';
import NoteNode from './node/note-node';
import { RelevantNode } from './node/relevant-node';
import { RetrievalNode } from './node/retrieval-node';
import { RewriteNode } from './node/rewrite-node';
import { SwitchNode } from './node/switch-node';
const nodeTypes = {
ragNode: RagNode,
@ -33,6 +39,12 @@ const nodeTypes = {
relevantNode: RelevantNode,
logicNode: LogicNode,
noteNode: NoteNode,
switchNode: SwitchNode,
generateNode: GenerateNode,
retrievalNode: RetrievalNode,
messageNode: MessageNode,
rewriteNode: RewriteNode,
keywordNode: KeywordNode,
};
const edgeTypes = {

View File

@ -1,25 +1,24 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Flex } from 'antd';
import classNames from 'classnames';
import lowerFirst from 'lodash/lowerFirst';
import { useTranslation } from 'react-i18next';
import { Handle, NodeProps, Position } from 'reactflow';
import { Operator, operatorMap } from '../../constant';
import { NodeData } from '../../interface';
import OperatorIcon from '../../operator-icon';
import { RightHandleStyle } from './handle-icon';
import styles from './index.less';
// TODO: do not allow other nodes to connect to this node
export function BeginNode({ id, data, selected }: NodeProps<NodeData>) {
const { t } = useTranslate('flow');
export function BeginNode({ selected, data }: NodeProps<NodeData>) {
const { t } = useTranslation();
return (
<section
className={classNames(styles.ragNode, {
[styles.selectedNode]: selected,
})}
style={{
backgroundColor: operatorMap[data.label as Operator].backgroundColor,
color: 'white',
width: 50,
height: 50,
width: 100,
}}
>
<Handle
@ -27,13 +26,17 @@ export function BeginNode({ id, data, selected }: NodeProps<NodeData>) {
position={Position.Right}
isConnectable
className={styles.handle}
style={RightHandleStyle}
></Handle>
<Flex vertical align="center" justify="center" gap={6}>
<span className={styles.type}>{t(lowerFirst(data.label))}</span>
<Flex align="center" justify={'space-around'}>
<OperatorIcon
name={data.label as Operator}
fontSize={24}
color={operatorMap[data.label as Operator].color}
></OperatorIcon>
<div className={styles.nodeTitle}>{t(`flow.begin`)}</div>
</Flex>
<section className={styles.bottomBox}>
<div className={styles.nodeName}>{data.name}</div>
</section>
</section>
);
}

View File

@ -1,22 +1,17 @@
import { useTranslate } from '@/hooks/common-hooks';
import LLMLabel from '@/components/llm-select/llm-label';
import { Flex } from 'antd';
import classNames from 'classnames';
import lowerFirst from 'lodash/lowerFirst';
import { get } from 'lodash';
import { Handle, NodeProps, Position } from 'reactflow';
import { Operator, SwitchElseTo, operatorMap } from '../../constant';
import { NodeData } from '../../interface';
import OperatorIcon from '../../operator-icon';
import CategorizeHandle from './categorize-handle';
import NodeDropdown from './dropdown';
import { RightHandleStyle } from './handle-icon';
import { useBuildCategorizeHandlePositions } from './hooks';
import styles from './index.less';
import NodeHeader from './node-header';
import NodePopover from './popover';
export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) {
const style = operatorMap[data.label as Operator];
const { t } = useTranslate('flow');
const { positions } = useBuildCategorizeHandlePositions({ data, id });
const operatorName = data.label;
return (
<NodePopover nodeId={id}>
@ -24,10 +19,6 @@ export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) {
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
style={{
backgroundColor: style.backgroundColor,
color: style.color,
}}
>
<Handle
type="target"
@ -36,47 +27,35 @@ export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) {
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>
{operatorName === Operator.Switch && (
<CategorizeHandle top={50} right={-4} id={SwitchElseTo}>
To
</CategorizeHandle>
)}
{positions.map((position, idx) => {
return (
<CategorizeHandle
top={position.top}
right={position.right}
key={idx}
id={position.text}
idx={idx}
></CategorizeHandle>
);
})}
<Flex vertical align="center" justify="center" gap={6}>
<OperatorIcon
name={data.label as Operator}
fontSize={24}
></OperatorIcon>
<span className={styles.type}>{t(lowerFirst(data.label))}</span>
<NodeDropdown id={id}></NodeDropdown>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<Flex vertical gap={8}>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
{positions.map((position, idx) => {
return (
<div key={idx}>
<div className={styles.nodeText}>{position.text}</div>
<Handle
key={position.text}
id={position.text}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
style={{ ...RightHandleStyle, top: position.top }}
></Handle>
</div>
);
})}
</Flex>
<section className={styles.bottomBox}>
<div className={styles.nodeName}>{data.name}</div>
</section>
</section>
</NodePopover>
);

View File

@ -38,7 +38,7 @@ const NodeDropdown = ({ id, iconFontColor }: IProps) => {
return (
<OperateDropdown
iconFontSize={14}
iconFontSize={22}
height={14}
deleteItem={deleteNode}
items={items}

View File

@ -0,0 +1,74 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { Handle, NodeProps, Position } from 'reactflow';
import { useGetComponentLabelByValue } from '../../hooks';
import { IGenerateParameter, NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
import NodePopover from './popover';
export function GenerateNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const parameters: IGenerateParameter[] = get(data, 'form.parameters', []);
const getLabel = useGetComponentLabelByValue(id);
return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<Flex gap={8} vertical className={styles.generateParameters}>
{parameters.map((x) => (
<Flex
key={x.id}
align="center"
gap={6}
className={styles.conditionBlock}
>
<label htmlFor="">{x.key}</label>
<span className={styles.parameterValue}>
{getLabel(x.component_id)}
</span>
</Flex>
))}
</Flex>
</section>
</NodePopover>
);
}

View File

@ -0,0 +1,20 @@
import { PlusOutlined } from '@ant-design/icons';
import { CSSProperties } from 'react';
export const HandleIcon = () => {
return (
<PlusOutlined
style={{ fontSize: 6, color: 'white', position: 'absolute', zIndex: 10 }}
/>
);
};
export const RightHandleStyle: CSSProperties = {
right: -5,
};
export const LeftHandleStyle: CSSProperties = {
left: -7,
};
export default HandleIcon;

View File

@ -1,14 +1,13 @@
import get from 'lodash/get';
import pick from 'lodash/pick';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo } from 'react';
import { useUpdateNodeInternals } from 'reactflow';
import { Operator } from '../../constant';
import { IPosition, NodeData } from '../../interface';
import { SwitchElseTo } from '../../constant';
import {
buildNewPositionMap,
generateSwitchHandleText,
isKeysEqual,
} from '../../utils';
ICategorizeItemResult,
ISwitchCondition,
NodeData,
} from '../../interface';
import { generateSwitchHandleText } from '../../utils';
export const useBuildCategorizeHandlePositions = ({
data,
@ -17,85 +16,86 @@ export const useBuildCategorizeHandlePositions = ({
id: string;
data: NodeData;
}) => {
const operatorName = data.label as Operator;
const updateNodeInternals = useUpdateNodeInternals();
const [positionMap, setPositionMap] = useState<Record<string, IPosition>>({});
const categoryData = useMemo(() => {
if (operatorName === Operator.Categorize) {
return get(data, `form.category_description`, {});
} else if (operatorName === Operator.Switch) {
return get(data, 'form.conditions', []);
}
return {};
}, [data, operatorName]);
const categoryData: ICategorizeItemResult = useMemo(() => {
return get(data, `form.category_description`, {});
}, [data]);
const positions = useMemo(() => {
return Object.keys(categoryData)
.map((x, idx) => {
const position = positionMap[x];
let text = x;
if (operatorName === Operator.Switch) {
text = generateSwitchHandleText(idx);
}
return { text, ...position };
})
.filter((x) => typeof x?.right === 'number');
}, [categoryData, positionMap, operatorName]);
const list: Array<{
text: string;
top: number;
idx: number;
}> = [];
useEffect(() => {
// Cache used coordinates
setPositionMap((state) => {
const categoryDataKeys = Object.keys(categoryData);
const stateKeys = Object.keys(state);
if (!isKeysEqual(categoryDataKeys, stateKeys)) {
const { newPositionMap, intersectionKeys } = buildNewPositionMap(
categoryDataKeys,
state,
);
const nextPositionMap = {
...pick(state, intersectionKeys),
...newPositionMap,
};
return nextPositionMap;
}
return state;
Object.keys(categoryData).forEach((x, idx) => {
list.push({
text: x,
idx,
top: idx === 0 ? 98 : list[idx - 1].top + 8 + 26,
});
});
return list;
}, [categoryData]);
useEffect(() => {
updateNodeInternals(id);
}, [id, updateNodeInternals, positionMap]);
}, [id, updateNodeInternals, categoryData]);
return { positions };
};
// export const useBuildSwitchHandlePositions = ({
// data,
// id,
// }: {
// id: string;
// data: NodeData;
// }) => {
// const [positionMap, setPositionMap] = useState<Record<string, IPosition>>({});
// const conditions = useMemo(() => get(data, 'form.conditions', []), [data]);
// const updateNodeInternals = useUpdateNodeInternals();
export const useBuildSwitchHandlePositions = ({
data,
id,
}: {
id: string;
data: NodeData;
}) => {
const updateNodeInternals = useUpdateNodeInternals();
// const positions = useMemo(() => {
// return conditions
// .map((x, idx) => {
// const text = `Item ${idx}`;
// const position = positionMap[text];
// return { text: text, ...position };
// })
// .filter((x) => typeof x?.right === 'number');
// }, [conditions, positionMap]);
const conditions: ISwitchCondition[] = useMemo(() => {
return get(data, 'form.conditions', []);
}, [data]);
// useEffect(() => {
// updateNodeInternals(id);
// }, [id, updateNodeInternals, positionMap]);
const positions = useMemo(() => {
const list: Array<{
text: string;
top: number;
idx: number;
condition?: ISwitchCondition;
}> = [];
// return { positions };
// };
[...conditions, ''].forEach((x, idx) => {
let top = idx === 0 ? 58 : list[idx - 1].top + 32; // case number (Case 1) height + flex gap
if (idx - 1 >= 0) {
const previousItems = conditions[idx - 1]?.items ?? [];
if (previousItems.length > 0) {
top += 12; // ConditionBlock padding
top += previousItems.length * 22; // condition variable height
top += (previousItems.length - 1) * 25; // operator height
}
}
list.push({
text:
idx < conditions.length
? generateSwitchHandleText(idx)
: SwitchElseTo,
idx,
top,
condition: typeof x === 'string' ? undefined : x,
});
});
return list;
}, [conditions]);
useEffect(() => {
updateNodeInternals(id);
}, [id, updateNodeInternals, conditions]);
return { positions };
};

View File

@ -3,22 +3,16 @@
-6px 0 12px 0 rgba(179, 177, 177, 0.08),
-3px 0 6px -4px rgba(0, 0, 0, 0.12),
-6px 0 16px 6px rgba(0, 0, 0, 0.05);
padding: 10px;
border-radius: 10px;
background: white;
width: 200px;
}
.ragNode {
position: relative;
.commonNode();
padding: 5px;
border-radius: 5px;
background: white;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
// align-items: center;
// justify-self: center;
justify-content: center;
.nodeName {
font-size: 10px;
color: black;
@ -28,23 +22,10 @@
color: #777;
font-size: 12px;
}
.type {
// font-size: 12px;
}
.description {
font-size: 10px;
}
.bottomBox {
position: absolute;
bottom: -34px;
background: white;
padding: 2px 5px;
border-radius: 5px;
box-shadow:
-6px 0 12px 0 rgba(179, 177, 177, 0.08),
-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;
@ -53,14 +34,25 @@
}
}
@lightBackgroundColor: rgba(150, 150, 150, 0.1);
@darkBackgroundColor: rgba(150, 150, 150, 0.2);
.selectedNode {
border: 1px solid rgb(59, 118, 244);
border: 1.5px solid rgb(59, 118, 244);
}
.handle {
display: inline-flex;
text-align: center;
// align-items: center;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
background: rgb(59, 88, 253);
border: 1px solid white;
z-index: 1;
background-image: url('@/assets/svg/plus.svg');
background-size: cover;
background-position: center;
}
.jsonView {
@ -71,19 +63,8 @@
}
.logicNode {
position: relative;
.commonNode();
padding: 5px;
border-radius: 5px;
background: white;
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
// align-items: center;
// justify-self: center;
justify-content: center;
.nodeName {
font-size: 10px;
color: black;
@ -93,41 +74,122 @@
color: #777;
font-size: 12px;
}
.type {
// font-size: 12px;
}
.description {
font-size: 10px;
}
.bottomBox {
position: absolute;
bottom: -34px;
background: white;
padding: 2px 5px;
border-radius: 5px;
box-shadow:
-6px 0 12px 0 rgba(179, 177, 177, 0.08),
-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;
}
.relevantSourceLabel {
font-size: 10px;
}
}
.noteNode {
.commonNode();
width: 140px;
padding: 4px 6px 6px;
min-width: 140px;
width: auto;
height: 100%;
padding: 0;
border-radius: 10px;
background-color: #dbf8f4;
min-height: 128px;
.noteTitle {
background-color: #edfcff;
font-size: 12px;
padding: 6px 6px 4px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.noteForm {
margin-top: 4px;
height: calc(100% - 50px);
}
.noteName {
padding: 0px 4px;
}
.noteTextarea {
resize: none;
border: 0;
border-radius: 0;
height: 100%;
&:focus {
border: none;
box-shadow: none;
}
}
}
.nodeText {
padding-inline: 0.4em;
padding-block: 0.2em 0.1em;
background: @lightBackgroundColor;
border-radius: 3px;
min-height: 22px;
.textEllipsis();
}
.nodeTitle {
font-weight: 600;
text-align: center;
.textEllipsis();
}
.nodeHeader {
padding-bottom: 12px;
}
.zeroDivider {
margin: 0 !important;
}
.conditionBlock {
border-radius: 4px;
padding: 6px;
background: @lightBackgroundColor;
}
.conditionLine {
border-radius: 4px;
padding: 0 4px;
background: @darkBackgroundColor;
.textEllipsis();
}
.conditionKey {
flex: 1;
}
.conditionOperator {
padding: 0 2px;
text-align: center;
}
.relevantLabel {
text-align: right;
}
.knowledgeNodeName {
.textEllipsis();
}
.messageNodeContainer {
overflow-y: auto;
max-height: 300px;
}
.generateParameters {
padding-top: 8px;
label {
flex: 2;
.textEllipsis();
}
.parameterValue {
flex: 3;
.conditionLine;
}
}

View File

@ -1,12 +1,9 @@
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 { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
import NodePopover from './popover';
export function RagNode({
@ -15,17 +12,12 @@ export function RagNode({
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const style = operatorMap[data.label as Operator];
return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.ragNode, {
[styles.selectedNode]: selected,
})}
style={{
...pick(style, ['backgroundColor', 'color']),
}}
>
<Handle
id="c"
@ -33,39 +25,17 @@ export function RagNode({
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle type="source" position={Position.Top} id="d" isConnectable />
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
id="b"
style={RightHandleStyle}
></Handle>
<Handle type="source" position={Position.Bottom} id="a" isConnectable />
<Flex vertical align="center" justify={'space-around'}>
<Flex flex={1} justify="center" align="center">
<label htmlFor=""> </label>
</Flex>
<Flex flex={1}>
<OperatorIcon
name={data.label as Operator}
fontSize={style?.iconFontSize ?? 16}
width={style?.iconWidth}
></OperatorIcon>
</Flex>
<Flex flex={1}>
<NodeDropdown
id={id}
iconFontColor={style?.moreIconColor}
></NodeDropdown>
</Flex>
</Flex>
<section className={styles.bottomBox}>
<div className={styles.nodeName}>{data.name}</div>
</section>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</section>
</NodePopover>
);

View File

@ -0,0 +1,54 @@
import LLMLabel from '@/components/llm-select/llm-label';
import classNames from 'classnames';
import { get } from 'lodash';
import { Handle, NodeProps, Position } from 'reactflow';
import { NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
import NodePopover from './popover';
export function KeywordNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
</section>
</NodePopover>
);
}

View File

@ -1,38 +1,23 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Flex } from 'antd';
import classNames from 'classnames';
import lowerFirst from 'lodash/lowerFirst';
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 { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
import NodePopover from './popover';
const ZeroGapOperators = [
Operator.RewriteQuestion,
Operator.KeywordExtract,
Operator.ArXiv,
];
export function LogicNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const style = operatorMap[data.label as Operator];
const { t } = useTranslate('flow');
return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
style={pick(style, ['backgroundColor', 'width', 'height', 'color'])}
>
<Handle
id="c"
@ -40,49 +25,17 @@ export function LogicNode({
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle type="source" position={Position.Top} id="d" isConnectable />
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<Handle type="source" position={Position.Bottom} id="a" isConnectable />
<Flex
vertical
align="center"
justify={'space-around'}
gap={ZeroGapOperators.some((x) => x === data.label) ? 0 : 6}
>
<Flex flex={1} justify="center" align="center">
<OperatorIcon
name={data.label as Operator}
fontSize={style?.iconFontSize ?? 24}
width={style?.iconWidth}
></OperatorIcon>
</Flex>
<Flex flex={1}>
<span
className={styles.type}
style={{ fontSize: style?.fontSize ?? 14 }}
>
{t(lowerFirst(data.label))}
</span>
</Flex>
<Flex flex={1}>
<NodeDropdown
id={id}
iconFontColor={style?.moreIconColor}
></NodeDropdown>
</Flex>
</Flex>
<section className={styles.bottomBox}>
<div className={styles.nodeName}>{data.name}</div>
</section>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</section>
</NodePopover>
);

View File

@ -0,0 +1,63 @@
import { Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { Handle, NodeProps, Position } from 'reactflow';
import { NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
import NodePopover from './popover';
export function MessageNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const messages: string[] = get(data, 'form.messages', []);
return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={classNames({
[styles.nodeHeader]: messages.length > 0,
})}
></NodeHeader>
<Flex vertical gap={8} className={styles.messageNodeContainer}>
{messages.map((message, idx) => {
return (
<div className={styles.nodeText} key={idx}>
{message}
</div>
);
})}
</Flex>
</section>
</NodePopover>
);
}

View File

@ -0,0 +1,35 @@
import { Flex } from 'antd';
import { Operator, operatorMap } from '../../constant';
import OperatorIcon from '../../operator-icon';
import NodeDropdown from './dropdown';
import styles from './index.less';
interface IProps {
id: string;
label: string;
name: string;
gap?: number;
className?: string;
}
const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => {
return (
<Flex
flex={1}
align="center"
justify={'space-between'}
gap={gap}
className={className}
>
<OperatorIcon
name={label as Operator}
color={operatorMap[label as Operator].color}
></OperatorIcon>
<span className={styles.nodeTitle}>{name}</span>
<NodeDropdown id={id}></NodeDropdown>
</Flex>
);
};
export default NodeHeader;

View File

@ -1,20 +1,33 @@
import { Flex, Form, Input, Space } from 'antd';
import { NodeProps } from 'reactflow';
import { Flex, Form, Input } from 'antd';
import classNames from 'classnames';
import { NodeProps, NodeResizeControl } from 'reactflow';
import { NodeData } from '../../interface';
import NodeDropdown from './dropdown';
import SvgIcon from '@/components/svg-icon';
import { useEffect } from 'react';
import { memo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useHandleFormValuesChange } from '../../hooks';
import {
useHandleFormValuesChange,
useHandleNodeNameChange,
} from '../../hooks';
import styles from './index.less';
const { TextArea } = Input;
const controlStyle = {
background: 'transparent',
border: 'none',
};
function NoteNode({ data, id }: NodeProps<NodeData>) {
const { t } = useTranslation();
const [form] = Form.useForm();
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
id,
data,
});
const { handleValuesChange } = useHandleFormValuesChange(id);
useEffect(() => {
@ -22,25 +35,51 @@ function NoteNode({ data, id }: NodeProps<NodeData>) {
}, [form, data?.form]);
return (
<section className={styles.noteNode}>
<Flex justify={'space-between'}>
<Space size={'small'}>
<>
<NodeResizeControl style={controlStyle} minWidth={190} minHeight={128}>
<SvgIcon
name="resize"
width={12}
style={{
position: 'absolute',
right: 5,
bottom: 5,
cursor: 'nwse-resize',
}}
></SvgIcon>
</NodeResizeControl>
<section className={styles.noteNode}>
<Flex
justify={'space-between'}
className={classNames(styles.noteTitle, 'note-drag-handle')}
align="center"
gap={6}
>
<SvgIcon name="note" width={14}></SvgIcon>
<span className={styles.noteTitle}>{t('flow.note')}</span>
</Space>
<NodeDropdown id={id}></NodeDropdown>
</Flex>
<Form
onValuesChange={handleValuesChange}
form={form}
className={styles.noteForm}
>
<Form.Item name="text" noStyle>
<TextArea rows={3} placeholder={t('flow.notePlaceholder')} />
</Form.Item>
</Form>
</section>
<Input
value={name ?? t('flow.note')}
onBlur={handleNameBlur}
onChange={handleNameChange}
className={styles.noteName}
></Input>
<NodeDropdown id={id}></NodeDropdown>
</Flex>
<Form
onValuesChange={handleValuesChange}
form={form}
className={styles.noteForm}
>
<Form.Item name="text" noStyle>
<TextArea
rows={3}
placeholder={t('flow.notePlaceholder')}
className={styles.noteTextarea}
/>
</Form.Item>
</Form>
</section>
</>
);
}
export default NoteNode;
export default memo(NoteNode);

View File

@ -1,28 +1,23 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Flex } from 'antd';
import classNames from 'classnames';
import lowerFirst from 'lodash/lowerFirst';
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';
import { RightHandleStyle } from './handle-icon';
import NodePopover from './popover';
import { get } from 'lodash';
import styles from './index.less';
import NodeHeader from './node-header';
export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) {
const style = operatorMap[data.label as Operator];
const { t } = useTranslate('flow');
const yes = get(data, 'form.yes');
const no = get(data, 'form.no');
return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
style={pick(style, ['backgroundColor', 'width', 'height', 'color'])}
>
<Handle
type="target"
@ -32,43 +27,38 @@ export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) {
id={'a'}
></Handle>
<Handle
type="target"
position={Position.Top}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
id={'b'}
id={'yes'}
style={{ ...RightHandleStyle, top: 59 }}
></Handle>
<Handle
type="target"
position={Position.Bottom}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
id={'c'}
id={'no'}
style={{ ...RightHandleStyle, top: 112 }}
></Handle>
<CategorizeHandle top={20} right={6} id={'yes'}></CategorizeHandle>
<CategorizeHandle top={80} right={6} id={'no'}></CategorizeHandle>
<Flex vertical align="center" justify="center" gap={0}>
<Flex flex={1}>
<OperatorIcon
name={data.label as Operator}
fontSize={style.iconFontSize}
></OperatorIcon>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<Flex vertical gap={10}>
<Flex vertical>
<div className={styles.relevantLabel}>Yes</div>
<div className={styles.nodeText}>{yes}</div>
</Flex>
<Flex flex={1}>
<span
className={styles.type}
style={{ fontSize: style.fontSize ?? 14 }}
>
{t(lowerFirst(data.label))}
</span>
</Flex>
<Flex flex={1}>
<NodeDropdown id={id}></NodeDropdown>
<Flex vertical>
<div className={styles.relevantLabel}>No</div>
<div className={styles.nodeText}>{no}</div>
</Flex>
</Flex>
<section className={styles.bottomBox}>
<div className={styles.nodeName}>{data.name}</div>
</section>
</section>
</NodePopover>
);

View File

@ -0,0 +1,85 @@
import { useNextFetchKnowledgeList } from '@/hooks/knowledge-hooks';
import { UserOutlined } from '@ant-design/icons';
import { Avatar, Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { useMemo } from 'react';
import { Handle, NodeProps, Position } from 'reactflow';
import { NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
import NodePopover from './popover';
export function RetrievalNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []);
const { list: knowledgeList } = useNextFetchKnowledgeList(true);
const knowledgeBases = useMemo(() => {
return knowledgeBaseIds.map((x) => {
const item = knowledgeList.find((y) => x === y.id);
return {
name: item?.name,
avatar: item?.avatar,
id: x,
};
});
}, [knowledgeList, knowledgeBaseIds]);
return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={classNames({
[styles.nodeHeader]: knowledgeBaseIds.length > 0,
})}
></NodeHeader>
<Flex vertical gap={8}>
{knowledgeBases.map((knowledge) => {
return (
<div className={styles.nodeText} key={knowledge.id}>
<Flex align={'center'} gap={6}>
<Avatar
size={26}
icon={<UserOutlined />}
src={knowledge.avatar}
/>
<Flex className={styles.knowledgeNodeName} flex={1}>
{knowledge.name}
</Flex>
</Flex>
</div>
);
})}
</Flex>
</section>
</NodePopover>
);
}

View File

@ -0,0 +1,54 @@
import LLMLabel from '@/components/llm-select/llm-label';
import classNames from 'classnames';
import { get } from 'lodash';
import { Handle, NodeProps, Position } from 'reactflow';
import { NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
import NodePopover from './popover';
export function RewriteNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
</section>
</NodePopover>
);
}

View File

@ -0,0 +1,112 @@
import { Divider, Flex } from 'antd';
import classNames from 'classnames';
import { Handle, NodeProps, Position } from 'reactflow';
import { useGetComponentLabelByValue } from '../../hooks';
import { ISwitchCondition, NodeData } from '../../interface';
import { RightHandleStyle } from './handle-icon';
import { useBuildSwitchHandlePositions } from './hooks';
import styles from './index.less';
import NodeHeader from './node-header';
import NodePopover from './popover';
const getConditionKey = (idx: number, length: number) => {
if (idx === 0 && length !== 1) {
return 'If';
} else if (idx === length - 1) {
return 'Else';
}
return 'ElseIf';
};
const ConditionBlock = ({
condition,
nodeId,
}: {
condition: ISwitchCondition;
nodeId: string;
}) => {
const items = condition?.items ?? [];
const getLabel = useGetComponentLabelByValue(nodeId);
return (
<Flex vertical className={styles.conditionBlock}>
{items.map((x, idx) => (
<div key={idx}>
<Flex>
<div
className={classNames(styles.conditionLine, styles.conditionKey)}
>
{getLabel(x?.cpn_id)}
</div>
<span className={styles.conditionOperator}>{x?.operator}</span>
<Flex flex={1} className={styles.conditionLine}>
{x?.value}
</Flex>
</Flex>
{idx + 1 < items.length && (
<Divider orientationMargin="0" className={styles.zeroDivider}>
{condition?.logical_operator}
</Divider>
)}
</div>
))}
</Flex>
);
};
export function SwitchNode({ id, data, selected }: NodeProps<NodeData>) {
const { positions } = useBuildSwitchHandlePositions({ data, id });
return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
type="target"
position={Position.Left}
isConnectable
className={styles.handle}
id={'a'}
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<Flex vertical gap={10}>
{positions.map((position, idx) => {
return (
<div key={idx}>
<Flex vertical>
<Flex justify={'space-between'}>
<span>{idx < positions.length - 1 && position.text}</span>
<span>{getConditionKey(idx, positions.length)}</span>
</Flex>
{position.condition && (
<ConditionBlock
nodeId={id}
condition={position.condition}
></ConditionBlock>
)}
</Flex>
<Handle
key={position.text}
id={position.text}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
style={{ ...RightHandleStyle, top: position.top }}
></Handle>
</div>
);
})}
</Flex>
</section>
</NodePopover>
);
}