### 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
| <div | <div | ||||
| ref={ref} | ref={ref} | ||||
| className={cn( | className={cn( | ||||
| 'relative rounded-md border bg-card text-card-foreground', | |||||
| 'relative rounded-md bg-card text-card-foreground', | |||||
| className, | className, | ||||
| selected ? 'border-muted-foreground shadow-lg' : '', | selected ? 'border-muted-foreground shadow-lg' : '', | ||||
| 'hover:ring-1', | 'hover:ring-1', |
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { IAgentNode } from '@/interfaces/database/flow'; | import { IAgentNode } from '@/interfaces/database/flow'; | ||||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | import { Handle, NodeProps, Position } from '@xyflow/react'; | ||||
| import classNames from 'classnames'; | |||||
| import { memo, useMemo } from 'react'; | import { memo, useMemo } from 'react'; | ||||
| import { Operator } from '../../constant'; | import { Operator } from '../../constant'; | ||||
| import useGraphStore from '../../store'; | import useGraphStore from '../../store'; | ||||
| import { CommonHandle } from './handle'; | |||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | 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({ | function InnerAgentNode({ | ||||
| id, | id, | ||||
| isConnectable = true, | isConnectable = true, | ||||
| selected, | selected, | ||||
| }: NodeProps<IAgentNode>) { | }: NodeProps<IAgentNode>) { | ||||
| const { theme } = useTheme(); | |||||
| const getNode = useGraphStore((state) => state.getNode); | const getNode = useGraphStore((state) => state.getNode); | ||||
| const edges = useGraphStore((state) => state.edges); | const edges = useGraphStore((state) => state.edges); | ||||
| }, [edges, getNode, id]); | }, [edges, getNode, id]); | ||||
| return ( | 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 && ( | {isNotParentAgent && ( | ||||
| <> | <> | ||||
| <Handle | |||||
| <CommonHandle | |||||
| id="c" | id="c" | ||||
| type="source" | type="source" | ||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| className={styles.handle} | |||||
| style={LeftHandleStyle} | style={LeftHandleStyle} | ||||
| ></Handle> | |||||
| <Handle | |||||
| ></CommonHandle> | |||||
| <CommonHandle | |||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable={isConnectable} | isConnectable={isConnectable} | ||||
| className={styles.handle} | className={styles.handle} | ||||
| id="b" | id="b" | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| ></Handle> | |||||
| ></CommonHandle> | |||||
| </> | </> | ||||
| )} | )} | ||||
| <Handle | <Handle | ||||
| style={{ left: 180 }} | style={{ left: 180 }} | ||||
| ></Handle> | ></Handle> | ||||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | ||||
| </section> | |||||
| </NodeWrapper> | |||||
| </ToolBar> | </ToolBar> | ||||
| ); | ); | ||||
| } | } |
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { IBeginNode } from '@/interfaces/database/flow'; | import { IBeginNode } from '@/interfaces/database/flow'; | ||||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||||
| import { NodeProps, Position } from '@xyflow/react'; | |||||
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | |||||
| import get from 'lodash/get'; | import get from 'lodash/get'; | ||||
| import { memo } from 'react'; | import { memo } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| BeginQueryType, | BeginQueryType, | ||||
| BeginQueryTypeIconMap, | BeginQueryTypeIconMap, | ||||
| Operator, | Operator, | ||||
| operatorMap, | |||||
| } from '../../constant'; | } from '../../constant'; | ||||
| import { BeginQuery } from '../../interface'; | import { BeginQuery } from '../../interface'; | ||||
| import OperatorIcon from '../../operator-icon'; | import OperatorIcon from '../../operator-icon'; | ||||
| import { CommonHandle } from './handle'; | |||||
| import { RightHandleStyle } from './handle-icon'; | import { RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import { NodeWrapper } from './node-wrapper'; | |||||
| // TODO: do not allow other nodes to connect to this node | // 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 { t } = useTranslation(); | ||||
| const query: BeginQuery[] = get(data, 'form.query', []); | const query: BeginQuery[] = get(data, 'form.query', []); | ||||
| const { theme } = useTheme(); | |||||
| return ( | return ( | ||||
| <section | |||||
| className={classNames( | |||||
| styles.ragNode, | |||||
| theme === 'dark' ? styles.dark : '', | |||||
| { | |||||
| [styles.selectedNode]: selected, | |||||
| }, | |||||
| )} | |||||
| > | |||||
| <Handle | |||||
| <NodeWrapper> | |||||
| <CommonHandle | |||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable | isConnectable | ||||
| className={styles.handle} | className={styles.handle} | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| ></Handle> | |||||
| ></CommonHandle> | |||||
| <Flex align="center" justify={'center'} gap={10}> | <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"> | <div className="truncate text-center font-semibold text-sm"> | ||||
| {t(`flow.begin`)} | {t(`flow.begin`)} | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| })} | })} | ||||
| </Flex> | </Flex> | ||||
| </section> | |||||
| </NodeWrapper> | |||||
| ); | ); | ||||
| } | } | ||||
| 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> | |||||
| ); | |||||
| } |
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import NodeHeader from './node-header'; | import NodeHeader from './node-header'; | ||||
| import { ToolBar } from './toolbar'; | |||||
| function InnerRagNode({ | function InnerRagNode({ | ||||
| id, | id, | ||||
| }: NodeProps<IRagNode>) { | }: NodeProps<IRagNode>) { | ||||
| const { theme } = useTheme(); | const { theme } = useTheme(); | ||||
| return ( | 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> | |||||
| ); | ); | ||||
| } | } | ||||
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { IMessageNode } from '@/interfaces/database/flow'; | import { IMessageNode } from '@/interfaces/database/flow'; | ||||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||||
| import { NodeProps, Position } from '@xyflow/react'; | |||||
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| import { memo } from 'react'; | import { memo } from 'react'; | ||||
| import { CommonHandle } from './handle'; | |||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import NodeHeader from './node-header'; | import NodeHeader from './node-header'; | ||||
| import { NodeWrapper } from './node-wrapper'; | |||||
| import { ToolBar } from './toolbar'; | |||||
| function InnerMessageNode({ | function InnerMessageNode({ | ||||
| id, | id, | ||||
| selected, | selected, | ||||
| }: NodeProps<IMessageNode>) { | }: NodeProps<IMessageNode>) { | ||||
| const messages: string[] = get(data, 'form.messages', []); | const messages: string[] = get(data, 'form.messages', []); | ||||
| const { theme } = useTheme(); | |||||
| return ( | 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> | |||||
| ); | ); | ||||
| } | } | ||||
| 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 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 { | interface IProps { | ||||
| id: string; | id: string; | ||||
| label: string; | label: string; | ||||
| wrapperClassName?: string; | 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 = ({ | const InnerNodeHeader = ({ | ||||
| label, | label, | ||||
| id, | |||||
| name, | name, | ||||
| gap = 4, | |||||
| className, | className, | ||||
| wrapperClassName, | wrapperClassName, | ||||
| }: IProps) => { | }: IProps) => { | ||||
| return ( | 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"> | <span className="truncate text-center font-semibold text-sm"> | ||||
| {name} | {name} | ||||
| </span> | </span> | ||||
| <NodeDropdown id={id} label={label}></NodeDropdown> | |||||
| </Flex> | |||||
| </div> | |||||
| </section> | </section> | ||||
| ); | ); | ||||
| }; | }; | ||||
| const NodeHeader = memo(InnerNodeHeader); | const NodeHeader = memo(InnerNodeHeader); | ||||
| export default NodeHeader; | 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> | |||||
| ); | |||||
| } |
| 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> | |||||
| ); | |||||
| } |
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | ||||
| import { IRetrievalNode } from '@/interfaces/database/flow'; | import { IRetrievalNode } from '@/interfaces/database/flow'; | ||||
| import { UserOutlined } from '@ant-design/icons'; | 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 { Avatar, Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| import { memo, useMemo } from 'react'; | import { memo, useMemo } from 'react'; | ||||
| import { CommonHandle } from './handle'; | |||||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import NodeHeader from './node-header'; | import NodeHeader from './node-header'; | ||||
| import { NodeWrapper } from './node-wrapper'; | |||||
| import { ToolBar } from './toolbar'; | |||||
| function InnerRetrievalNode({ | function InnerRetrievalNode({ | ||||
| id, | id, | ||||
| selected, | selected, | ||||
| }: NodeProps<IRetrievalNode>) { | }: NodeProps<IRetrievalNode>) { | ||||
| const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []); | const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []); | ||||
| const { theme } = useTheme(); | |||||
| const { list: knowledgeList } = useFetchKnowledgeList(true); | const { list: knowledgeList } = useFetchKnowledgeList(true); | ||||
| const knowledgeBases = useMemo(() => { | const knowledgeBases = useMemo(() => { | ||||
| return knowledgeBaseIds.map((x) => { | return knowledgeBaseIds.map((x) => { | ||||
| }, [knowledgeList, knowledgeBaseIds]); | }, [knowledgeList, knowledgeBaseIds]); | ||||
| return ( | 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> | ||||
| </Flex> | |||||
| </div> | |||||
| ); | |||||
| })} | |||||
| </Flex> | |||||
| </section> | |||||
| </div> | |||||
| ); | |||||
| })} | |||||
| </Flex> | |||||
| </NodeWrapper> | |||||
| </ToolBar> | |||||
| ); | ); | ||||
| } | } | ||||
| import { IconFont } from '@/components/icon-font'; | import { IconFont } from '@/components/icon-font'; | ||||
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { Card, CardContent } from '@/components/ui/card'; | import { Card, CardContent } from '@/components/ui/card'; | ||||
| import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow'; | 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 { memo, useCallback } from 'react'; | ||||
| import { SwitchOperatorOptions } from '../../constant'; | import { SwitchOperatorOptions } from '../../constant'; | ||||
| import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; | import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; | ||||
| import { CommonHandle } from './handle'; | |||||
| import { RightHandleStyle } from './handle-icon'; | import { RightHandleStyle } from './handle-icon'; | ||||
| import { useBuildSwitchHandlePositions } from './hooks'; | 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) => { | const getConditionKey = (idx: number, length: number) => { | ||||
| if (idx === 0 && length !== 1) { | if (idx === 0 && length !== 1) { | ||||
| function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { | function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { | ||||
| const { positions } = useBuildSwitchHandlePositions({ data, id }); | const { positions } = useBuildSwitchHandlePositions({ data, id }); | ||||
| const { theme } = useTheme(); | |||||
| return ( | 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" | type="target" | ||||
| position={Position.Left} | position={Position.Left} | ||||
| isConnectable | isConnectable | ||||
| className={styles.handle} | |||||
| id={'a'} | 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"> | <section className="gap-2.5 flex flex-col"> | ||||
| {positions.map((position, idx) => { | {positions.map((position, idx) => { | ||||
| return ( | return ( | ||||
| ></ConditionBlock> | ></ConditionBlock> | ||||
| )} | )} | ||||
| </section> | </section> | ||||
| <Handle | |||||
| <CommonHandle | |||||
| key={position.text} | key={position.text} | ||||
| id={position.text} | id={position.text} | ||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} | ||||
| isConnectable | isConnectable | ||||
| className={styles.handle} | |||||
| style={{ ...RightHandleStyle, top: position.top }} | style={{ ...RightHandleStyle, top: position.top }} | ||||
| ></Handle> | |||||
| ></CommonHandle> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| })} | })} | ||||
| </section> | </section> | ||||
| </section> | |||||
| </NodeWrapper> | |||||
| </ToolBar> | </ToolBar> | ||||
| ); | ); | ||||
| } | } |
| 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> | |||||
| ); | |||||
| } |
| } | } | ||||
| export const OperatorIconMap = { | export const OperatorIconMap = { | ||||
| [Operator.Retrieval]: 'retrival-0', | |||||
| [Operator.Retrieval]: 'KR', | |||||
| // [Operator.Generate]: MergeCellsOutlined, | // [Operator.Generate]: MergeCellsOutlined, | ||||
| // [Operator.Answer]: SendOutlined, | // [Operator.Answer]: SendOutlined, | ||||
| [Operator.Begin]: CirclePlay, | [Operator.Begin]: CirclePlay, |