diff --git a/web/src/components/xyflow/base-node.tsx b/web/src/components/xyflow/base-node.tsx new file mode 100644 index 000000000..5ecc1b4d9 --- /dev/null +++ b/web/src/components/xyflow/base-node.tsx @@ -0,0 +1,22 @@ +import { forwardRef, HTMLAttributes } from 'react'; + +import { cn } from '@/lib/utils'; + +export const BaseNode = forwardRef< + HTMLDivElement, + HTMLAttributes & { selected?: boolean } +>(({ className, selected, ...props }, ref) => ( +
+)); + +BaseNode.displayName = 'BaseNode'; diff --git a/web/src/components/xyflow/tooltip-node.tsx b/web/src/components/xyflow/tooltip-node.tsx new file mode 100644 index 000000000..b53c67617 --- /dev/null +++ b/web/src/components/xyflow/tooltip-node.tsx @@ -0,0 +1,101 @@ +import { NodeProps, NodeToolbar, NodeToolbarProps } from '@xyflow/react'; +import { + HTMLAttributes, + ReactNode, + createContext, + forwardRef, + useCallback, + useContext, + useState, +} from 'react'; +import { BaseNode } from './base-node'; + +/* TOOLTIP CONTEXT ---------------------------------------------------------- */ + +const TooltipContext = createContext(false); + +/* TOOLTIP NODE ------------------------------------------------------------- */ + +export type TooltipNodeProps = Partial & { + children?: ReactNode; +}; + +/** + * A component that wraps a node and provides tooltip visibility context. + */ +export const TooltipNode = forwardRef( + ({ selected, children }, ref) => { + const [isTooltipVisible, setTooltipVisible] = useState(false); + + const showTooltip = useCallback(() => setTooltipVisible(true), []); + const hideTooltip = useCallback(() => setTooltipVisible(false), []); + + return ( + + + {children} + + + ); + }, +); + +TooltipNode.displayName = 'TooltipNode'; + +/* TOOLTIP CONTENT ---------------------------------------------------------- */ + +export type TooltipContentProps = NodeToolbarProps; + +/** + * A component that displays the tooltip content based on visibility context. + */ +export const TooltipContent = forwardRef( + ({ position, children }, ref) => { + const isTooltipVisible = useContext(TooltipContext); + + return ( +
+ + {children} + +
+ ); + }, +); + +TooltipContent.displayName = 'TooltipContent'; + +/* TOOLTIP TRIGGER ---------------------------------------------------------- */ + +export type TooltipTriggerProps = HTMLAttributes; + +/** + * A component that triggers the tooltip visibility. + */ +export const TooltipTrigger = forwardRef< + HTMLParagraphElement, + TooltipTriggerProps +>(({ children, ...props }, ref) => { + return ( +
+ {children} +
+ ); +}); + +TooltipTrigger.displayName = 'TooltipTrigger'; diff --git a/web/src/pages/agent/canvas/node/agent-node.tsx b/web/src/pages/agent/canvas/node/agent-node.tsx index 528c077ce..090a19b37 100644 --- a/web/src/pages/agent/canvas/node/agent-node.tsx +++ b/web/src/pages/agent/canvas/node/agent-node.tsx @@ -7,7 +7,7 @@ import { Operator } from '../../constant'; import useGraphStore from '../../store'; import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import styles from './index.less'; -import NodeHeader from './node-header'; +import NodeHeader, { ToolBar } from './node-header'; function InnerAgentNode({ id, @@ -26,50 +26,52 @@ function InnerAgentNode({ }, [edges, getNode, id]); return ( -
- {isNotParentAgent && ( - <> - - - - )} - - - -
+ +
+ {isNotParentAgent && ( + <> + + + + )} + + + +
+
); } diff --git a/web/src/pages/agent/canvas/node/hooks.ts b/web/src/pages/agent/canvas/node/hooks.ts index fbea8f166..116cae119 100644 --- a/web/src/pages/agent/canvas/node/hooks.ts +++ b/web/src/pages/agent/canvas/node/hooks.ts @@ -72,13 +72,13 @@ export const useBuildSwitchHandlePositions = ({ }> = []; [...conditions, ''].forEach((x, idx) => { - let top = idx === 0 ? 58 + 20 : list[idx - 1].top + 32; // case number (Case 1) height + flex gap + let top = idx === 0 ? 58 + 20 : list[idx - 1].top + 10; // 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 += 12; // ConditionBlock padding top += previousItems.length * 22; // condition variable height - top += (previousItems.length - 1) * 25; // operator height + // top += (previousItems.length - 1) * 25; // operator height } } diff --git a/web/src/pages/agent/canvas/node/node-header.tsx b/web/src/pages/agent/canvas/node/node-header.tsx index 29847e7a5..216657103 100644 --- a/web/src/pages/agent/canvas/node/node-header.tsx +++ b/web/src/pages/agent/canvas/node/node-header.tsx @@ -1,13 +1,19 @@ import { useTranslate } from '@/hooks/common-hooks'; import { Flex } from 'antd'; -import { Play } from 'lucide-react'; +import { Copy, Play, Trash2 } from 'lucide-react'; import { Operator, operatorMap } from '../../constant'; import OperatorIcon from '../../operator-icon'; import { needsSingleStepDebugging } from '../../utils'; import NodeDropdown from './dropdown'; import { NextNodePopover } from './popover'; -import { memo } from 'react'; +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; @@ -74,3 +80,37 @@ const InnerNodeHeader = ({ const NodeHeader = memo(InnerNodeHeader); export default NodeHeader; + +function IconWrapper({ children }: PropsWithChildren) { + return ( +
+ {children} +
+ ); +} + +type ToolBarProps = { + selected?: boolean | undefined; +} & PropsWithChildren; + +export function ToolBar({ selected, children }: ToolBarProps) { + return ( + + {children} + + +
+ + + + + + + + + +
+
+
+ ); +} diff --git a/web/src/pages/agent/canvas/node/switch-node.tsx b/web/src/pages/agent/canvas/node/switch-node.tsx index 97b4ce79f..a7c25dcfa 100644 --- a/web/src/pages/agent/canvas/node/switch-node.tsx +++ b/web/src/pages/agent/canvas/node/switch-node.tsx @@ -3,7 +3,6 @@ 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 { Flex } from 'antd'; import classNames from 'classnames'; import { memo, useCallback } from 'react'; import { SwitchOperatorOptions } from '../../constant'; @@ -11,7 +10,7 @@ import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; import { RightHandleStyle } from './handle-icon'; import { useBuildSwitchHandlePositions } from './hooks'; import styles from './index.less'; -import NodeHeader from './node-header'; +import NodeHeader, { ToolBar } from './node-header'; const getConditionKey = (idx: number, length: number) => { if (idx === 0 && length !== 1) { @@ -40,10 +39,10 @@ const ConditionBlock = ({ return ( - + {items.map((x, idx) => (
-
+
{getLabel(x?.cpn_id)}
@@ -61,61 +60,64 @@ function InnerSwitchNode({ id, data, selected }: NodeProps) { const { positions } = useBuildSwitchHandlePositions({ data, id }); const { theme } = useTheme(); return ( -
- - - - {positions.map((position, idx) => { - return ( -
- - - - {idx < positions.length - 1 && - position.condition?.logical_operator?.toUpperCase()} - - {getConditionKey(idx, positions.length)} - - {position.condition && ( - - )} - - -
- ); - })} -
-
+ +
+ + +
+ {positions.map((position, idx) => { + return ( +
+
+
+ + {idx < positions.length - 1 && + position.condition?.logical_operator?.toUpperCase()} + + {getConditionKey(idx, positions.length)} +
+ {position.condition && ( + + )} +
+ +
+ ); + })} +
+
+
); } diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant.tsx index e0709bf73..d093f1a27 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant.tsx @@ -1,32 +1,3 @@ -import { - GitHubIcon, - KeywordIcon, - QWeatherIcon, - WikipediaIcon, -} from '@/assets/icon/Icon'; -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'; -import { ReactComponent as DeepLIcon } from '@/assets/svg/deepl.svg'; -import { ReactComponent as DuckIcon } from '@/assets/svg/duck.svg'; -import { ReactComponent as EmailIcon } from '@/assets/svg/email.svg'; -import { ReactComponent as ExeSqlIcon } from '@/assets/svg/exesql.svg'; -import { ReactComponent as GoogleScholarIcon } from '@/assets/svg/google-scholar.svg'; -import { ReactComponent as GoogleIcon } from '@/assets/svg/google.svg'; -import { ReactComponent as InvokeIcon } from '@/assets/svg/invoke-ai.svg'; -import { ReactComponent as Jin10Icon } from '@/assets/svg/jin10.svg'; -import { ReactComponent as NoteIcon } from '@/assets/svg/note.svg'; -import { ReactComponent as PubMedIcon } from '@/assets/svg/pubmed.svg'; -import { ReactComponent as SwitchIcon } from '@/assets/svg/switch.svg'; -import { ReactComponent as TemplateIcon } from '@/assets/svg/template.svg'; -import { ReactComponent as TuShareIcon } from '@/assets/svg/tushare.svg'; -import { ReactComponent as WenCaiIcon } from '@/assets/svg/wencai.svg'; -import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.svg'; import { initialKeywordsSimilarityWeightValue, initialSimilarityThresholdValue, @@ -61,24 +32,10 @@ export enum PromptRole { Assistant = 'assistant', } -import { - BranchesOutlined, - DatabaseOutlined, - FormOutlined, - MergeCellsOutlined, - MessageOutlined, - RocketOutlined, - SendOutlined, -} from '@ant-design/icons'; import upperFirst from 'lodash/upperFirst'; import { - Box, - CirclePower, CloudUpload, - CodeXml, - IterationCcw, ListOrdered, - MessageSquareMore, OptionIcon, TextCursorInput, ToggleLeft, @@ -152,48 +109,6 @@ export const AgentOperatorList = [ Operator.Agent, ]; -export const operatorIconMap = { - [Operator.Retrieval]: RocketOutlined, - [Operator.Generate]: MergeCellsOutlined, - [Operator.Answer]: SendOutlined, - [Operator.Begin]: BeginIcon, - [Operator.Categorize]: DatabaseOutlined, - [Operator.Message]: MessageOutlined, - [Operator.Relevant]: BranchesOutlined, - [Operator.RewriteQuestion]: FormOutlined, - [Operator.KeywordExtract]: KeywordIcon, - [Operator.DuckDuckGo]: DuckIcon, - [Operator.Baidu]: BaiduIcon, - [Operator.Wikipedia]: WikipediaIcon, - [Operator.PubMed]: PubMedIcon, - [Operator.ArXiv]: ArXivIcon, - [Operator.Google]: GoogleIcon, - [Operator.Bing]: BingIcon, - [Operator.GoogleScholar]: GoogleScholarIcon, - [Operator.DeepL]: DeepLIcon, - [Operator.GitHub]: GitHubIcon, - [Operator.BaiduFanyi]: baiduFanyiIcon, - [Operator.QWeather]: QWeatherIcon, - [Operator.ExeSQL]: ExeSqlIcon, - [Operator.Switch]: SwitchIcon, - [Operator.WenCai]: WenCaiIcon, - [Operator.AkShare]: AkShareIcon, - [Operator.YahooFinance]: YahooFinanceIcon, - [Operator.Jin10]: Jin10Icon, - [Operator.Concentrator]: ConcentratorIcon, - [Operator.TuShare]: TuShareIcon, - [Operator.Note]: NoteIcon, - [Operator.Crawler]: CrawlerIcon, - [Operator.Invoke]: InvokeIcon, - [Operator.Template]: TemplateIcon, - [Operator.Email]: EmailIcon, - [Operator.Iteration]: IterationCcw, - [Operator.IterationStart]: CirclePower, - [Operator.Code]: CodeXml, - [Operator.WaitingDialogue]: MessageSquareMore, - [Operator.Agent]: Box, -}; - export const operatorMap: Record< Operator, { diff --git a/web/src/pages/agent/form/switch-form/index.tsx b/web/src/pages/agent/form/switch-form/index.tsx index ef300b877..e70df46ce 100644 --- a/web/src/pages/agent/form/switch-form/index.tsx +++ b/web/src/pages/agent/form/switch-form/index.tsx @@ -296,6 +296,7 @@ const SwitchForm = ({ node }: IOperatorForm) => { operator: switchOperatorOptions[0].value, }, ], + to: [], }) } > diff --git a/web/src/pages/agent/operator-icon.tsx b/web/src/pages/agent/operator-icon.tsx index 593d617c4..e5498d668 100644 --- a/web/src/pages/agent/operator-icon.tsx +++ b/web/src/pages/agent/operator-icon.tsx @@ -1,24 +1,66 @@ -import { Operator, operatorIconMap } from './constant'; +import { IconFont } from '@/components/icon-font'; +import { cn } from '@/lib/utils'; +import { CirclePlay } from 'lucide-react'; +import { Operator } from './constant'; interface IProps { name: Operator; - fontSize?: number; - width?: number; - color?: string; + className?: string; } +export const OperatorIconMap = { + [Operator.Retrieval]: 'retrival-0', + // [Operator.Generate]: MergeCellsOutlined, + // [Operator.Answer]: SendOutlined, + [Operator.Begin]: CirclePlay, + [Operator.Categorize]: 'a-QuestionClassification', + [Operator.Message]: 'reply', + [Operator.Iteration]: 'loop', + [Operator.Switch]: 'condition', + [Operator.Code]: 'code-set', + [Operator.Agent]: 'agent-ai', + // [Operator.Relevant]: BranchesOutlined, + // [Operator.RewriteQuestion]: FormOutlined, + // [Operator.KeywordExtract]: KeywordIcon, + // [Operator.DuckDuckGo]: DuckIcon, + // [Operator.Baidu]: BaiduIcon, + // [Operator.Wikipedia]: WikipediaIcon, + // [Operator.PubMed]: PubMedIcon, + // [Operator.ArXiv]: ArXivIcon, + // [Operator.Google]: GoogleIcon, + // [Operator.Bing]: BingIcon, + // [Operator.GoogleScholar]: GoogleScholarIcon, + // [Operator.DeepL]: DeepLIcon, + // [Operator.GitHub]: GitHubIcon, + // [Operator.BaiduFanyi]: baiduFanyiIcon, + // [Operator.QWeather]: QWeatherIcon, + // [Operator.ExeSQL]: ExeSqlIcon, + // [Operator.WenCai]: WenCaiIcon, + // [Operator.AkShare]: AkShareIcon, + // [Operator.YahooFinance]: YahooFinanceIcon, + // [Operator.Jin10]: Jin10Icon, + // [Operator.Concentrator]: ConcentratorIcon, + // [Operator.TuShare]: TuShareIcon, + // [Operator.Note]: NoteIcon, + // [Operator.Crawler]: CrawlerIcon, + // [Operator.Invoke]: InvokeIcon, + // [Operator.Template]: TemplateIcon, + // [Operator.Email]: EmailIcon, + // [Operator.IterationStart]: CirclePower, + // [Operator.WaitingDialogue]: MessageSquareMore, +}; + const Empty = () => { return
; }; -const OperatorIcon = ({ name, fontSize, width, color }: IProps) => { - const Icon = operatorIconMap[name] || Empty; - return ( - +const OperatorIcon = ({ name, className }: IProps) => { + const Icon = OperatorIconMap[name as keyof typeof OperatorIconMap] || Empty; + + return typeof Icon === 'string' ? ( + + ) : ( + ); };