### What problem does this PR solve? Feat: Modify the style of the canvas operator node #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| @@ -9,7 +9,7 @@ export const BaseNode = forwardRef< | |||
| <div | |||
| ref={ref} | |||
| className={cn( | |||
| 'relative rounded-md border bg-card text-card-foreground', | |||
| 'relative rounded-md bg-card text-card-foreground', | |||
| className, | |||
| selected ? 'border-muted-foreground shadow-lg' : '', | |||
| 'hover:ring-1', | |||
| @@ -1,13 +1,14 @@ | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { IAgentNode } from '@/interfaces/database/flow'; | |||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||
| import classNames from 'classnames'; | |||
| import { memo, useMemo } from 'react'; | |||
| import { Operator } from '../../constant'; | |||
| import useGraphStore from '../../store'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| import NodeHeader, { ToolBar } from './node-header'; | |||
| import NodeHeader from './node-header'; | |||
| import { NodeWrapper } from './node-wrapper'; | |||
| import { ToolBar } from './toolbar'; | |||
| function InnerAgentNode({ | |||
| id, | |||
| @@ -15,7 +16,6 @@ function InnerAgentNode({ | |||
| isConnectable = true, | |||
| selected, | |||
| }: NodeProps<IAgentNode>) { | |||
| const { theme } = useTheme(); | |||
| const getNode = useGraphStore((state) => state.getNode); | |||
| const edges = useGraphStore((state) => state.edges); | |||
| @@ -26,34 +26,25 @@ function InnerAgentNode({ | |||
| }, [edges, getNode, id]); | |||
| return ( | |||
| <ToolBar selected={selected}> | |||
| <section | |||
| className={classNames( | |||
| styles.ragNode, | |||
| theme === 'dark' ? styles.dark : '', | |||
| { | |||
| [styles.selectedNode]: selected, | |||
| }, | |||
| )} | |||
| > | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <NodeWrapper> | |||
| {isNotParentAgent && ( | |||
| <> | |||
| <Handle | |||
| <CommonHandle | |||
| id="c" | |||
| type="source" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={LeftHandleStyle} | |||
| ></Handle> | |||
| <Handle | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| id="b" | |||
| style={RightHandleStyle} | |||
| ></Handle> | |||
| ></CommonHandle> | |||
| </> | |||
| )} | |||
| <Handle | |||
| @@ -70,7 +61,7 @@ function InnerAgentNode({ | |||
| style={{ left: 180 }} | |||
| ></Handle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| </section> | |||
| </NodeWrapper> | |||
| </ToolBar> | |||
| ); | |||
| } | |||
| @@ -1,8 +1,6 @@ | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { IBeginNode } from '@/interfaces/database/flow'; | |||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import get from 'lodash/get'; | |||
| import { memo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| @@ -10,42 +8,31 @@ import { | |||
| BeginQueryType, | |||
| BeginQueryTypeIconMap, | |||
| Operator, | |||
| operatorMap, | |||
| } from '../../constant'; | |||
| import { BeginQuery } from '../../interface'; | |||
| import OperatorIcon from '../../operator-icon'; | |||
| import { CommonHandle } from './handle'; | |||
| import { RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| import { NodeWrapper } from './node-wrapper'; | |||
| // TODO: do not allow other nodes to connect to this node | |||
| function InnerBeginNode({ selected, data }: NodeProps<IBeginNode>) { | |||
| function InnerBeginNode({ data }: NodeProps<IBeginNode>) { | |||
| const { t } = useTranslation(); | |||
| const query: BeginQuery[] = get(data, 'form.query', []); | |||
| const { theme } = useTheme(); | |||
| return ( | |||
| <section | |||
| className={classNames( | |||
| styles.ragNode, | |||
| theme === 'dark' ? styles.dark : '', | |||
| { | |||
| [styles.selectedNode]: selected, | |||
| }, | |||
| )} | |||
| > | |||
| <Handle | |||
| <NodeWrapper> | |||
| <CommonHandle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable | |||
| className={styles.handle} | |||
| style={RightHandleStyle} | |||
| ></Handle> | |||
| ></CommonHandle> | |||
| <Flex align="center" justify={'center'} gap={10}> | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| fontSize={24} | |||
| color={operatorMap[data.label as Operator].color} | |||
| ></OperatorIcon> | |||
| <OperatorIcon name={data.label as Operator}></OperatorIcon> | |||
| <div className="truncate text-center font-semibold text-sm"> | |||
| {t(`flow.begin`)} | |||
| </div> | |||
| @@ -68,7 +55,7 @@ function InnerBeginNode({ selected, data }: NodeProps<IBeginNode>) { | |||
| ); | |||
| })} | |||
| </Flex> | |||
| </section> | |||
| </NodeWrapper> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| import { cn } from '@/lib/utils'; | |||
| import { Handle, HandleProps } from '@xyflow/react'; | |||
| import { Plus } from 'lucide-react'; | |||
| export function CommonHandle({ className, ...props }: HandleProps) { | |||
| return ( | |||
| <Handle | |||
| {...props} | |||
| className={cn( | |||
| 'inline-flex justify-center items-center !bg-background-checked !size-4 !rounded-sm !border-none ', | |||
| className, | |||
| )} | |||
| > | |||
| <Plus className="size-3 pointer-events-none" /> | |||
| </Handle> | |||
| ); | |||
| } | |||
| @@ -6,6 +6,7 @@ import { memo } from 'react'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| import NodeHeader from './node-header'; | |||
| import { ToolBar } from './toolbar'; | |||
| function InnerRagNode({ | |||
| id, | |||
| @@ -15,33 +16,35 @@ function InnerRagNode({ | |||
| }: NodeProps<IRagNode>) { | |||
| const { theme } = useTheme(); | |||
| return ( | |||
| <section | |||
| className={classNames( | |||
| styles.ragNode, | |||
| theme === 'dark' ? styles.dark : '', | |||
| { | |||
| [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} | |||
| id="b" | |||
| style={RightHandleStyle} | |||
| ></Handle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| </section> | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <section | |||
| className={classNames( | |||
| styles.ragNode, | |||
| theme === 'dark' ? styles.dark : '', | |||
| { | |||
| [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} | |||
| id="b" | |||
| style={RightHandleStyle} | |||
| ></Handle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| </section> | |||
| </ToolBar> | |||
| ); | |||
| } | |||
| @@ -1,13 +1,15 @@ | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { IMessageNode } from '@/interfaces/database/flow'; | |||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { get } from 'lodash'; | |||
| import { memo } from 'react'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| import NodeHeader from './node-header'; | |||
| import { NodeWrapper } from './node-wrapper'; | |||
| import { ToolBar } from './toolbar'; | |||
| function InnerMessageNode({ | |||
| id, | |||
| @@ -16,52 +18,43 @@ function InnerMessageNode({ | |||
| selected, | |||
| }: NodeProps<IMessageNode>) { | |||
| const messages: string[] = get(data, 'form.messages', []); | |||
| const { theme } = useTheme(); | |||
| return ( | |||
| <section | |||
| className={classNames( | |||
| styles.logicNode, | |||
| theme === 'dark' ? styles.dark : '', | |||
| { | |||
| [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> | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <NodeWrapper> | |||
| <CommonHandle | |||
| id="c" | |||
| type="source" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| style={LeftHandleStyle} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| style={RightHandleStyle} | |||
| id="b" | |||
| ></CommonHandle> | |||
| <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> | |||
| <Flex vertical gap={8} className={styles.messageNodeContainer}> | |||
| {messages.map((message, idx) => { | |||
| return ( | |||
| <div className={styles.nodeText} key={idx}> | |||
| {message} | |||
| </div> | |||
| ); | |||
| })} | |||
| </Flex> | |||
| </NodeWrapper> | |||
| </ToolBar> | |||
| ); | |||
| } | |||
| @@ -1,20 +1,7 @@ | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { Flex } from 'antd'; | |||
| import { Copy, Play, Trash2 } from 'lucide-react'; | |||
| import { Operator, operatorMap } from '../../constant'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { memo } from 'react'; | |||
| import { Operator } from '../../constant'; | |||
| import OperatorIcon from '../../operator-icon'; | |||
| import { needsSingleStepDebugging } from '../../utils'; | |||
| import NodeDropdown from './dropdown'; | |||
| import { NextNodePopover } from './popover'; | |||
| import { | |||
| TooltipContent, | |||
| TooltipNode, | |||
| TooltipTrigger, | |||
| } from '@/components/xyflow/tooltip-node'; | |||
| import { Position } from '@xyflow/react'; | |||
| import { PropsWithChildren, memo } from 'react'; | |||
| import { RunTooltip } from '../../flow-tooltip'; | |||
| interface IProps { | |||
| id: string; | |||
| label: string; | |||
| @@ -24,55 +11,20 @@ interface IProps { | |||
| wrapperClassName?: string; | |||
| } | |||
| const ExcludedRunStateOperators = [Operator.Answer]; | |||
| export function RunStatus({ id, name, label }: IProps) { | |||
| const { t } = useTranslate('flow'); | |||
| return ( | |||
| <section className="flex justify-end items-center pb-1 gap-2 text-blue-600"> | |||
| {needsSingleStepDebugging(label) && ( | |||
| <RunTooltip> | |||
| <Play className="size-3 cursor-pointer" data-play /> | |||
| </RunTooltip> // data-play is used to trigger single step debugging | |||
| )} | |||
| <NextNodePopover nodeId={id} name={name}> | |||
| <span className="cursor-pointer text-[10px]"> | |||
| {t('operationResults')} | |||
| </span> | |||
| </NextNodePopover> | |||
| </section> | |||
| ); | |||
| } | |||
| const InnerNodeHeader = ({ | |||
| label, | |||
| id, | |||
| name, | |||
| gap = 4, | |||
| className, | |||
| wrapperClassName, | |||
| }: IProps) => { | |||
| return ( | |||
| <section className={wrapperClassName}> | |||
| {!ExcludedRunStateOperators.includes(label as Operator) && ( | |||
| <RunStatus id={id} name={name} label={label}></RunStatus> | |||
| )} | |||
| <Flex | |||
| flex={1} | |||
| align="center" | |||
| justify={'space-between'} | |||
| gap={gap} | |||
| className={className} | |||
| > | |||
| <OperatorIcon | |||
| name={label as Operator} | |||
| color={operatorMap[label as Operator]?.color} | |||
| ></OperatorIcon> | |||
| <section className={cn(wrapperClassName, 'pb-4')}> | |||
| <div className={cn(className, 'flex gap-2.5')}> | |||
| <OperatorIcon name={label as Operator}></OperatorIcon> | |||
| <span className="truncate text-center font-semibold text-sm"> | |||
| {name} | |||
| </span> | |||
| <NodeDropdown id={id} label={label}></NodeDropdown> | |||
| </Flex> | |||
| </div> | |||
| </section> | |||
| ); | |||
| }; | |||
| @@ -80,37 +32,3 @@ const InnerNodeHeader = ({ | |||
| const NodeHeader = memo(InnerNodeHeader); | |||
| export default NodeHeader; | |||
| function IconWrapper({ children }: PropsWithChildren) { | |||
| return ( | |||
| <div className="p-1.5 bg-text-title rounded-sm cursor-pointer"> | |||
| {children} | |||
| </div> | |||
| ); | |||
| } | |||
| type ToolBarProps = { | |||
| selected?: boolean | undefined; | |||
| } & PropsWithChildren; | |||
| export function ToolBar({ selected, children }: ToolBarProps) { | |||
| return ( | |||
| <TooltipNode selected={selected}> | |||
| <TooltipTrigger>{children}</TooltipTrigger> | |||
| <TooltipContent position={Position.Top}> | |||
| <section className="flex gap-2 items-center"> | |||
| <IconWrapper> | |||
| <Play className="size-3.5" /> | |||
| </IconWrapper> | |||
| <IconWrapper> | |||
| <Copy className="size-3.5" /> | |||
| </IconWrapper> | |||
| <IconWrapper> | |||
| <Trash2 className="size-3.5" /> | |||
| </IconWrapper> | |||
| </section> | |||
| </TooltipContent> | |||
| </TooltipNode> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| import { cn } from '@/lib/utils'; | |||
| import { HTMLAttributes, PropsWithChildren } from 'react'; | |||
| export function NodeWrapper({ | |||
| children, | |||
| className, | |||
| }: PropsWithChildren & HTMLAttributes<HTMLDivElement>) { | |||
| return ( | |||
| <section | |||
| className={cn( | |||
| 'bg-background-header-bar p-2.5 rounded-md w-[200px]', | |||
| className, | |||
| )} | |||
| > | |||
| {children} | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -1,15 +1,17 @@ | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | |||
| import { IRetrievalNode } from '@/interfaces/database/flow'; | |||
| import { UserOutlined } from '@ant-design/icons'; | |||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { Avatar, Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { get } from 'lodash'; | |||
| import { memo, useMemo } from 'react'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| import NodeHeader from './node-header'; | |||
| import { NodeWrapper } from './node-wrapper'; | |||
| import { ToolBar } from './toolbar'; | |||
| function InnerRetrievalNode({ | |||
| id, | |||
| @@ -18,7 +20,6 @@ function InnerRetrievalNode({ | |||
| selected, | |||
| }: NodeProps<IRetrievalNode>) { | |||
| const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []); | |||
| const { theme } = useTheme(); | |||
| const { list: knowledgeList } = useFetchKnowledgeList(true); | |||
| const knowledgeBases = useMemo(() => { | |||
| return knowledgeBaseIds.map((x) => { | |||
| @@ -32,58 +33,52 @@ function InnerRetrievalNode({ | |||
| }, [knowledgeList, knowledgeBaseIds]); | |||
| return ( | |||
| <section | |||
| className={classNames( | |||
| styles.logicNode, | |||
| theme === 'dark' ? styles.dark : '', | |||
| { | |||
| [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} | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <NodeWrapper> | |||
| <CommonHandle | |||
| id="c" | |||
| type="source" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={LeftHandleStyle} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={RightHandleStyle} | |||
| id="b" | |||
| ></CommonHandle> | |||
| <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> | |||
| </Flex> | |||
| </div> | |||
| ); | |||
| })} | |||
| </Flex> | |||
| </section> | |||
| </div> | |||
| ); | |||
| })} | |||
| </Flex> | |||
| </NodeWrapper> | |||
| </ToolBar> | |||
| ); | |||
| } | |||
| @@ -1,16 +1,16 @@ | |||
| import { IconFont } from '@/components/icon-font'; | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow'; | |||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||
| import classNames from 'classnames'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { memo, useCallback } from 'react'; | |||
| import { SwitchOperatorOptions } from '../../constant'; | |||
| import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; | |||
| import { CommonHandle } from './handle'; | |||
| import { RightHandleStyle } from './handle-icon'; | |||
| import { useBuildSwitchHandlePositions } from './hooks'; | |||
| import styles from './index.less'; | |||
| import NodeHeader, { ToolBar } from './node-header'; | |||
| import NodeHeader from './node-header'; | |||
| import { NodeWrapper } from './node-wrapper'; | |||
| import { ToolBar } from './toolbar'; | |||
| const getConditionKey = (idx: number, length: number) => { | |||
| if (idx === 0 && length !== 1) { | |||
| @@ -58,32 +58,16 @@ const ConditionBlock = ({ | |||
| function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { | |||
| const { positions } = useBuildSwitchHandlePositions({ data, id }); | |||
| const { theme } = useTheme(); | |||
| return ( | |||
| <ToolBar selected={selected}> | |||
| <section | |||
| className={classNames( | |||
| styles.logicNode, | |||
| theme === 'dark' ? styles.dark : '', | |||
| { | |||
| [styles.selectedNode]: selected, | |||
| }, | |||
| 'group/operator hover:bg-slate-100', | |||
| )} | |||
| > | |||
| <Handle | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <NodeWrapper> | |||
| <CommonHandle | |||
| 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> | |||
| ></CommonHandle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| <section className="gap-2.5 flex flex-col"> | |||
| {positions.map((position, idx) => { | |||
| return ( | |||
| @@ -103,20 +87,19 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { | |||
| ></ConditionBlock> | |||
| )} | |||
| </section> | |||
| <Handle | |||
| <CommonHandle | |||
| key={position.text} | |||
| id={position.text} | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable | |||
| className={styles.handle} | |||
| style={{ ...RightHandleStyle, top: position.top }} | |||
| ></Handle> | |||
| ></CommonHandle> | |||
| </div> | |||
| ); | |||
| })} | |||
| </section> | |||
| </section> | |||
| </NodeWrapper> | |||
| </ToolBar> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,74 @@ | |||
| import { | |||
| TooltipContent, | |||
| TooltipNode, | |||
| TooltipTrigger, | |||
| } from '@/components/xyflow/tooltip-node'; | |||
| import { Position } from '@xyflow/react'; | |||
| import { Copy, Play, Trash2 } from 'lucide-react'; | |||
| import { MouseEventHandler, PropsWithChildren, useCallback } from 'react'; | |||
| import { Operator } from '../../constant'; | |||
| import { useDuplicateNode } from '../../hooks'; | |||
| import useGraphStore from '../../store'; | |||
| function IconWrapper({ children }: PropsWithChildren) { | |||
| return ( | |||
| <div className="p-1.5 bg-text-title rounded-sm cursor-pointer"> | |||
| {children} | |||
| </div> | |||
| ); | |||
| } | |||
| type ToolBarProps = { | |||
| selected?: boolean | undefined; | |||
| label: string; | |||
| id: string; | |||
| } & PropsWithChildren; | |||
| export function ToolBar({ selected, children, label, id }: ToolBarProps) { | |||
| const deleteNodeById = useGraphStore((store) => store.deleteNodeById); | |||
| const deleteIterationNodeById = useGraphStore( | |||
| (store) => store.deleteIterationNodeById, | |||
| ); | |||
| const deleteNode: MouseEventHandler<SVGElement> = useCallback( | |||
| (e) => { | |||
| e.stopPropagation(); | |||
| if (label === Operator.Iteration) { | |||
| deleteIterationNodeById(id); | |||
| } else { | |||
| deleteNodeById(id); | |||
| } | |||
| }, | |||
| [deleteIterationNodeById, deleteNodeById, id, label], | |||
| ); | |||
| const duplicateNode = useDuplicateNode(); | |||
| const handleDuplicate: MouseEventHandler<SVGElement> = useCallback( | |||
| (e) => { | |||
| e.stopPropagation(); | |||
| duplicateNode(id, label); | |||
| }, | |||
| [duplicateNode, id, label], | |||
| ); | |||
| return ( | |||
| <TooltipNode selected={selected}> | |||
| <TooltipTrigger>{children}</TooltipTrigger> | |||
| <TooltipContent position={Position.Top}> | |||
| <section className="flex gap-2 items-center"> | |||
| <IconWrapper> | |||
| <Play className="size-3.5" /> | |||
| </IconWrapper> | |||
| <IconWrapper> | |||
| <Copy className="size-3.5" onClick={handleDuplicate} /> | |||
| </IconWrapper> | |||
| <IconWrapper> | |||
| <Trash2 className="size-3.5" onClick={deleteNode} /> | |||
| </IconWrapper> | |||
| </section> | |||
| </TooltipContent> | |||
| </TooltipNode> | |||
| ); | |||
| } | |||
| @@ -9,7 +9,7 @@ interface IProps { | |||
| } | |||
| export const OperatorIconMap = { | |||
| [Operator.Retrieval]: 'retrival-0', | |||
| [Operator.Retrieval]: 'KR', | |||
| // [Operator.Generate]: MergeCellsOutlined, | |||
| // [Operator.Answer]: SendOutlined, | |||
| [Operator.Begin]: CirclePlay, | |||