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

View File

@ -2,6 +2,7 @@ import { ReactComponent as AkShareIcon } from '@/assets/svg/akshare.svg';
import { ReactComponent as ArXivIcon } from '@/assets/svg/arxiv.svg';
import { ReactComponent as baiduFanyiIcon } from '@/assets/svg/baidu-fanyi.svg';
import { ReactComponent as BaiduIcon } from '@/assets/svg/baidu.svg';
import { ReactComponent as BeginIcon } from '@/assets/svg/begin.svg';
import { ReactComponent as BingIcon } from '@/assets/svg/bing.svg';
import { ReactComponent as ConcentratorIcon } from '@/assets/svg/concentrator.svg';
import { ReactComponent as CrawlerIcon } from '@/assets/svg/crawler.svg';
@ -39,7 +40,6 @@ import {
MessageOutlined,
RocketOutlined,
SendOutlined,
SlidersOutlined,
} from '@ant-design/icons';
import upperFirst from 'lodash/upperFirst';
@ -85,7 +85,7 @@ export const operatorIconMap = {
[Operator.Retrieval]: RocketOutlined,
[Operator.Generate]: MergeCellsOutlined,
[Operator.Answer]: SendOutlined,
[Operator.Begin]: SlidersOutlined,
[Operator.Begin]: BeginIcon,
[Operator.Categorize]: DatabaseOutlined,
[Operator.Message]: MessageOutlined,
[Operator.Relevant]: BranchesOutlined,
@ -142,7 +142,7 @@ export const operatorMap: Record<
},
[Operator.Answer]: {
backgroundColor: '#f4816d',
color: 'white',
color: '#f4816d',
},
[Operator.Begin]: {
backgroundColor: '#4f51d6',
@ -157,7 +157,7 @@ export const operatorMap: Record<
},
[Operator.Relevant]: {
backgroundColor: '#9fd94d',
color: 'white',
color: '#8ef005',
width: 70,
height: 70,
fontSize: 12,
@ -165,7 +165,7 @@ export const operatorMap: Record<
},
[Operator.RewriteQuestion]: {
backgroundColor: '#f8c7f8',
color: 'white',
color: '#f32bf3',
width: 70,
height: 70,
fontSize: 12,
@ -175,7 +175,7 @@ export const operatorMap: Record<
width: 70,
height: 70,
backgroundColor: '#0f0e0f',
color: '#e1dcdc',
color: '#0f0e0f',
fontSize: 12,
iconWidth: 16,
// iconFontSize: 16,
@ -221,14 +221,14 @@ export const operatorMap: Record<
[Operator.BaiduFanyi]: { backgroundColor: '#e5f2d3' },
[Operator.QWeather]: { backgroundColor: '#a4bbf3' },
[Operator.ExeSQL]: { backgroundColor: '#b9efe8' },
[Operator.Switch]: { backgroundColor: '#dbaff6' },
[Operator.Switch]: { backgroundColor: '#dbaff6', color: '#dbaff6' },
[Operator.WenCai]: { backgroundColor: '#faac5b' },
[Operator.AkShare]: { backgroundColor: '#8085f5' },
[Operator.YahooFinance]: { backgroundColor: '#b474ff' },
[Operator.Jin10]: { backgroundColor: '#a0b9f8' },
[Operator.Concentrator]: {
backgroundColor: '#32d2a3',
color: 'white',
color: '#32d2a3',
width: 70,
height: 70,
fontSize: 10,
@ -586,18 +586,19 @@ export const RestrictedUpstreamMap = {
[Operator.Concentrator]: [Operator.Begin],
[Operator.TuShare]: [Operator.Begin],
[Operator.Crawler]: [Operator.Begin],
[Operator.Note]: [],
};
export const NodeMap = {
[Operator.Begin]: 'beginNode',
[Operator.Categorize]: 'categorizeNode',
[Operator.Retrieval]: 'logicNode',
[Operator.Generate]: 'logicNode',
[Operator.Retrieval]: 'retrievalNode',
[Operator.Generate]: 'generateNode',
[Operator.Answer]: 'logicNode',
[Operator.Message]: 'logicNode',
[Operator.Message]: 'messageNode',
[Operator.Relevant]: 'relevantNode',
[Operator.RewriteQuestion]: 'logicNode',
[Operator.KeywordExtract]: 'logicNode',
[Operator.RewriteQuestion]: 'rewriteNode',
[Operator.KeywordExtract]: 'keywordNode',
[Operator.DuckDuckGo]: 'ragNode',
[Operator.Baidu]: 'ragNode',
[Operator.Wikipedia]: 'ragNode',
@ -611,7 +612,7 @@ export const NodeMap = {
[Operator.BaiduFanyi]: 'ragNode',
[Operator.QWeather]: 'ragNode',
[Operator.ExeSQL]: 'ragNode',
[Operator.Switch]: 'categorizeNode',
[Operator.Switch]: 'switchNode',
[Operator.Concentrator]: 'logicNode',
[Operator.WenCai]: 'ragNode',
[Operator.AkShare]: 'ragNode',

View File

@ -7,3 +7,9 @@
font-weight: 600;
}
}
.operatorDescription {
font-size: 14px;
padding-top: 16px;
font-weight: normal;
}

View File

@ -3,7 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { Drawer, Flex, Form, Input } from 'antd';
import { useEffect } from 'react';
import { Node } from 'reactflow';
import { Operator } from '../constant';
import { Operator, operatorMap } from '../constant';
import AkShareForm from '../form/akshare-form';
import AnswerForm from '../form/answer-form';
import ArXivForm from '../form/arxiv-form';
@ -36,6 +36,8 @@ import YahooFinanceForm from '../form/yahoo-finance-form';
import { useHandleFormValuesChange, useHandleNodeNameChange } from '../hooks';
import OperatorIcon from '../operator-icon';
import { CloseOutlined } from '@ant-design/icons';
import { lowerFirst } from 'lodash';
import styles from './index.less';
interface IProps {
@ -74,7 +76,7 @@ const FormMap = {
[Operator.Crawler]: CrawlerForm,
};
const EmptyContent = () => <div>empty</div>;
const EmptyContent = () => <div></div>;
const FlowDrawer = ({
visible,
@ -84,8 +86,10 @@ const FlowDrawer = ({
const operatorName: Operator = node?.data.label;
const OperatorForm = FormMap[operatorName] ?? EmptyContent;
const [form] = Form.useForm();
const { name, handleNameBlur, handleNameChange } =
useHandleNodeNameChange(node);
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
id: node?.id,
data: node?.data,
});
const { t } = useTranslate('flow');
const { handleValuesChange } = useHandleFormValuesChange(node?.id);
@ -99,18 +103,27 @@ const FlowDrawer = ({
return (
<Drawer
title={
<Flex gap={'middle'} align="center">
<OperatorIcon name={operatorName}></OperatorIcon>
<Flex align="center" gap={'small'} flex={1}>
<label htmlFor="" className={styles.title}>
{t('title')}
</label>
<Input
value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
<Flex vertical>
<Flex gap={'middle'} align="center">
<OperatorIcon
name={operatorName}
color={operatorMap[operatorName]?.color}
></OperatorIcon>
<Flex align="center" gap={'small'} flex={1}>
<label htmlFor="" className={styles.title}>
{t('title')}
</label>
<Input
value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
</Flex>
<CloseOutlined onClick={hideModal} />
</Flex>
<span className={styles.operatorDescription}>
{t(`${lowerFirst(operatorName)}Description`)}
</span>
</Flex>
}
placement="right"
@ -119,6 +132,7 @@ const FlowDrawer = ({
getContainer={false}
mask={false}
width={470}
closeIcon={null}
>
<section className={styles.formWrapper}>
{visible && (

View File

@ -3,7 +3,7 @@ import { Card, Divider, Flex, Layout, Tooltip } from 'antd';
import classNames from 'classnames';
import lowerFirst from 'lodash/lowerFirst';
import React from 'react';
import { Operator, componentMenuList } from '../constant';
import { Operator, componentMenuList, operatorMap } from '../constant';
import { useHandleDrag } from '../hooks';
import OperatorIcon from '../operator-icon';
import styles from './index.less';
@ -53,7 +53,10 @@ const FlowSide = ({ setCollapsed, collapsed }: IProps) => {
onDragStart={handleDragStart(x.name)}
>
<Flex align="center" gap={15}>
<OperatorIcon name={x.name}></OperatorIcon>
<OperatorIcon
name={x.name}
color={operatorMap[x.name].color}
></OperatorIcon>
<section>
<Tooltip title={t(`${lowerFirst(x.name)}Description`)}>
<b>{t(lowerFirst(x.name))}</b>

View File

@ -3,19 +3,6 @@ import { useCallback, useMemo } from 'react';
import { Operator, RestrictedUpstreamMap } 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.Relevant]: [Operator.Begin, Operator.Answer, Operator.Relevant],
[Operator.Generate]: [Operator.Begin],
[Operator.Switch]: [Operator.Begin],
};
export const useBuildFormSelectOptions = (
operatorName: Operator,
selfId?: string, // exclude the current node
@ -24,8 +11,10 @@ export const useBuildFormSelectOptions = (
const buildCategorizeToOptions = useCallback(
(toList: string[]) => {
const excludedNodes: Operator[] =
RestrictedUpstreamMap[operatorName] ?? [];
const excludedNodes: Operator[] = [
Operator.Note,
...(RestrictedUpstreamMap[operatorName] ?? []),
];
return nodes
.filter(
(x) =>

View File

@ -1,6 +1,14 @@
import { useTranslate } from '@/hooks/common-hooks';
import { CloseOutlined } from '@ant-design/icons';
import { Button, Card, Form, FormListFieldData, Input, Select } from 'antd';
import {
Button,
Card,
Flex,
Form,
FormListFieldData,
Input,
Select,
} from 'antd';
import { FormInstance } from 'antd/lib';
import { humanId } from 'human-id';
import trim from 'lodash/trim';
@ -15,6 +23,8 @@ import { useUpdateNodeInternals } from 'reactflow';
import { Operator } from '../../constant';
import { useBuildFormSelectOptions } from '../../form-hooks';
import styles from './index.less';
interface IProps {
nodeId?: string;
}
@ -105,13 +115,12 @@ const DynamicCategorize = ({ nodeId }: IProps) => {
if (nodeId) updateNodeInternals(nodeId);
};
return (
<div
style={{ display: 'flex', rowGap: 10, flexDirection: 'column' }}
>
<Flex gap={18} vertical>
{fields.map((field) => (
<Card
size="small"
key={field.key}
className={styles.caseCard}
extra={
<CloseOutlined
onClick={() => {
@ -172,10 +181,15 @@ const DynamicCategorize = ({ nodeId }: IProps) => {
</Card>
))}
<Button type="dashed" onClick={handleAdd} block>
<Button
type="dashed"
onClick={handleAdd}
block
className={styles.addButton}
>
+ {t('addItem')}
</Button>
</div>
</Flex>
);
}}
</Form.List>

View File

@ -0,0 +1,11 @@
@lightBackgroundColor: rgba(150, 150, 150, 0.07);
@darkBackgroundColor: rgba(150, 150, 150, 0.12);
.caseCard {
background-color: @darkBackgroundColor;
}
.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}

View File

@ -90,6 +90,7 @@ const DynamicParameters = ({ nodeId }: IProps) => {
components={components}
rowClassName={() => styles.editableRow}
scroll={{ x: true }}
bordered
/>
</section>
);

View File

@ -0,0 +1,21 @@
@lightBackgroundColor: rgba(150, 150, 150, 0.07);
@darkBackgroundColor: rgba(150, 150, 150, 0.12);
.caseCard {
background-color: @lightBackgroundColor;
}
.conditionCard {
background-color: @darkBackgroundColor;
}
.elseCase {
background-color: @lightBackgroundColor;
padding: 12px;
border-radius: 8px;
}
.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}

View File

@ -13,6 +13,8 @@ import { useBuildComponentIdSelectOptions } from '../../hooks';
import { IOperatorForm, ISwitchForm } from '../../interface';
import { getOtherFieldValues } from '../../utils';
import styles from './index.less';
const SwitchForm = ({ onValuesChange, node, form }: IOperatorForm) => {
const { t } = useTranslation();
const buildCategorizeToOptions = useBuildFormSelectOptions(
@ -55,112 +57,134 @@ const SwitchForm = ({ onValuesChange, node, form }: IOperatorForm) => {
<Form.List name="conditions">
{(fields, { add, remove }) => (
<div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}>
{fields.map((field) => (
<Card
size="small"
title={`Case ${field.name + 1}`}
key={field.key}
extra={
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
}
>
<Form.Item noStyle dependencies={[field.name, 'items']}>
{({ getFieldValue }) =>
getFieldValue(['conditions', field.name, 'items'])?.length >
1 && (
<Form.Item
label={t('flow.logicalOperator')}
name={[field.name, 'logical_operator']}
>
<Select options={switchLogicOperatorOptions} />
</Form.Item>
)
{fields.map((field) => {
return (
<Card
size="small"
title={`Case ${field.name + 1}`}
key={field.key}
className={styles.caseCard}
extra={
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
}
</Form.Item>
<Form.Item label={t('flow.to')} name={[field.name, 'to']}>
<Select
allowClear
options={buildCategorizeToOptions([
form?.getFieldValue(SwitchElseTo),
...getOtherFieldValues(form!, 'conditions', field, 'to'),
])}
/>
</Form.Item>
<Form.Item label="Condition">
<Form.List name={[field.name, 'items']}>
{(subFields, subOpt) => (
<div
style={{
display: 'flex',
flexDirection: 'column',
rowGap: 16,
}}
>
{subFields.map((subField) => (
<Card
key={subField.key}
title={null}
size="small"
extra={
<CloseOutlined
onClick={() => {
subOpt.remove(subField.name);
}}
/>
}
>
<Form.Item
label={t('flow.componentId')}
name={[subField.name, 'cpn_id']}
>
<Select
placeholder={t('flow.componentId')}
options={componentIdOptions}
/>
</Form.Item>
<Form.Item
label={t('flow.operator')}
name={[subField.name, 'operator']}
>
<Select
placeholder={t('flow.operator')}
options={switchOperatorOptions}
/>
</Form.Item>
<Form.Item
label={t('flow.value')}
name={[subField.name, 'value']}
>
<Input placeholder={t('flow.value')} />
</Form.Item>
</Card>
))}
<Button
type="dashed"
onClick={() => subOpt.add()}
block
>
<Form.Item noStyle dependencies={[field.name, 'items']}>
{({ getFieldValue }) =>
getFieldValue(['conditions', field.name, 'items'])
?.length > 1 && (
<Form.Item
label={t('flow.logicalOperator')}
name={[field.name, 'logical_operator']}
>
+ Add Condition
</Button>
</div>
)}
</Form.List>
</Form.Item>
</Card>
))}
<Select options={switchLogicOperatorOptions} />
</Form.Item>
)
}
</Form.Item>
<Form.Item label={t('flow.to')} name={[field.name, 'to']}>
<Select
allowClear
options={buildCategorizeToOptions([
form?.getFieldValue(SwitchElseTo),
...getOtherFieldValues(
form!,
'conditions',
field,
'to',
),
])}
/>
</Form.Item>
<Form.Item label="Condition">
<Form.List name={[field.name, 'items']}>
{(subFields, subOpt) => (
<div
style={{
display: 'flex',
flexDirection: 'column',
rowGap: 16,
}}
>
{subFields.map((subField) => (
<Card
key={subField.key}
title={null}
size="small"
className={styles.conditionCard}
bordered
extra={
<CloseOutlined
onClick={() => {
subOpt.remove(subField.name);
}}
/>
}
>
<Form.Item
label={t('flow.componentId')}
name={[subField.name, 'cpn_id']}
>
<Select
placeholder={t('flow.componentId')}
options={componentIdOptions}
/>
</Form.Item>
<Form.Item
label={t('flow.operator')}
name={[subField.name, 'operator']}
>
<Select
placeholder={t('flow.operator')}
options={switchOperatorOptions}
/>
</Form.Item>
<Form.Item
label={t('flow.value')}
name={[subField.name, 'value']}
>
<Input placeholder={t('flow.value')} />
</Form.Item>
</Card>
))}
<Button
onClick={() => {
form?.setFieldValue(
['conditions', field.name, 'logical_operator'],
SwitchLogicOperatorOptions[0],
);
subOpt.add({
operator: SwitchOperatorOptions[0].value,
});
}}
block
className={styles.addButton}
>
+ Add Condition
</Button>
</div>
)}
</Form.List>
</Form.Item>
</Card>
);
})}
<Button type="dashed" onClick={() => add()} block>
<Button onClick={() => add()} block className={styles.addButton}>
+ Add Case
</Button>
</div>
)}
</Form.List>
<Divider />
<Form.Item label={'ELSE'} name={[SwitchElseTo]}>
<Form.Item
label={'ELSE'}
name={[SwitchElseTo]}
className={styles.elseCase}
>
<Select
allowClear
options={buildCategorizeToOptions(getSelectedConditionTos())}

View File

@ -69,6 +69,7 @@ import useGraphStore, { RFState } from './store';
import {
buildDslComponentsByGraph,
generateSwitchHandleText,
getNodeDragHandle,
receiveMessageError,
replaceIdWithText,
} from './utils';
@ -250,6 +251,7 @@ export const useHandleDrop = () => {
},
sourcePosition: Position.Right,
targetPosition: Position.Left,
dragHandle: getNodeDragHandle(type),
};
addNode(newNode);
@ -448,11 +450,16 @@ export const useValidateConnection = () => {
return isValidConnection;
};
export const useHandleNodeNameChange = (node?: Node) => {
export const useHandleNodeNameChange = ({
id,
data,
}: {
id?: string;
data: any;
}) => {
const [name, setName] = useState<string>('');
const { updateNodeName, nodes } = useGraphStore((state) => state);
const previousName = node?.data.name;
const id = node?.id;
const previousName = data?.name;
const handleNameBlur = useCallback(() => {
const existsSameName = nodes.some((x) => x.data.name === name);
@ -639,6 +646,7 @@ const ExcludedNodes = [
Operator.Relevant,
Operator.Begin,
Operator.Answer,
Operator.Note,
];
export const useBuildComponentIdSelectOptions = (nodeId?: string) => {
@ -655,3 +663,15 @@ export const useBuildComponentIdSelectOptions = (nodeId?: string) => {
return options;
};
export const useGetComponentLabelByValue = (nodeId: string) => {
const options = useBuildComponentIdSelectOptions(nodeId);
const getLabel = useCallback(
(val?: string) => {
return options.find((x) => x.value === val)?.label;
},
[options],
);
return getLabel;
};

View File

@ -64,20 +64,20 @@ export interface IRelevantForm extends IGenerateForm {
no: string;
}
interface Condition {
items: Item[];
export interface ISwitchCondition {
items: ISwitchItem[];
logical_operator: string;
to: string;
}
interface Item {
export interface ISwitchItem {
cpn_id: string;
operator: string;
value: string;
}
export interface ISwitchForm {
conditions: Condition[];
conditions: ISwitchCondition[];
end_cpn_id: string;
no: string;
}

View File

@ -7,12 +7,17 @@ interface IProps {
name: Operator;
fontSize?: number;
width?: number;
color?: string;
}
const OperatorIcon = ({ name, fontSize, width }: IProps) => {
const OperatorIcon = ({ name, fontSize, width, color }: IProps) => {
const Icon = operatorIconMap[name] || React.Fragment;
return (
<Icon className={styles.icon} style={{ fontSize }} width={width}></Icon>
<Icon
className={styles.icon}
style={{ fontSize, color }}
width={width}
></Icon>
);
};

View File

@ -23,7 +23,7 @@ import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { Operator, SwitchElseTo } from './constant';
import { NodeData } from './interface';
import { getOperatorIndex, isEdgeEqual } from './utils';
import { getNodeDragHandle, getOperatorIndex, isEdgeEqual } from './utils';
export type RFState = {
nodes: Node<NodeData>[];
@ -241,6 +241,7 @@ const useGraphStore = create<RFState>()(
dragging: false,
id: `${node?.data?.label}:${humanId()}`,
position,
dragHandle: getNodeDragHandle(node?.data?.label),
});
},
deleteEdge: () => {

View File

@ -236,3 +236,7 @@ export const getOtherFieldValues = (
export const generateSwitchHandleText = (idx: number) => {
return `Case ${idx + 1}`;
};
export const getNodeDragHandle = (nodeType?: string) => {
return nodeType === Operator.Note ? '.note-drag-handle' : undefined;
};