mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-22 22:26:43 +08:00
### 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:
@ -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 = {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -38,7 +38,7 @@ const NodeDropdown = ({ id, iconFontColor }: IProps) => {
|
||||
|
||||
return (
|
||||
<OperateDropdown
|
||||
iconFontSize={14}
|
||||
iconFontSize={22}
|
||||
height={14}
|
||||
deleteItem={deleteNode}
|
||||
items={items}
|
||||
|
||||
74
web/src/pages/flow/canvas/node/generate-node.tsx
Normal file
74
web/src/pages/flow/canvas/node/generate-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
20
web/src/pages/flow/canvas/node/handle-icon.tsx
Normal file
20
web/src/pages/flow/canvas/node/handle-icon.tsx
Normal 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;
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
54
web/src/pages/flow/canvas/node/keyword-node.tsx
Normal file
54
web/src/pages/flow/canvas/node/keyword-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
63
web/src/pages/flow/canvas/node/message-node.tsx
Normal file
63
web/src/pages/flow/canvas/node/message-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
web/src/pages/flow/canvas/node/node-header.tsx
Normal file
35
web/src/pages/flow/canvas/node/node-header.tsx
Normal 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;
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
85
web/src/pages/flow/canvas/node/retrieval-node.tsx
Normal file
85
web/src/pages/flow/canvas/node/retrieval-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
54
web/src/pages/flow/canvas/node/rewrite-node.tsx
Normal file
54
web/src/pages/flow/canvas/node/rewrite-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
112
web/src/pages/flow/canvas/node/switch-node.tsx
Normal file
112
web/src/pages/flow/canvas/node/switch-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -7,3 +7,9 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.operatorDescription {
|
||||
font-size: 14px;
|
||||
padding-top: 16px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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>
|
||||
|
||||
11
web/src/pages/flow/form/categorize-form/index.less
Normal file
11
web/src/pages/flow/form/categorize-form/index.less
Normal 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;
|
||||
}
|
||||
@ -90,6 +90,7 @@ const DynamicParameters = ({ nodeId }: IProps) => {
|
||||
components={components}
|
||||
rowClassName={() => styles.editableRow}
|
||||
scroll={{ x: true }}
|
||||
bordered
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
21
web/src/pages/flow/form/switch-form/index.less
Normal file
21
web/src/pages/flow/form/switch-form/index.less
Normal 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;
|
||||
}
|
||||
@ -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())}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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: () => {
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user