### What problem does this PR solve? Feat: Adjust the style of the agent canvas connection line #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| .edgeButton { | |||||
| width: 14px; | |||||
| height: 14px; | |||||
| background: #eee; | |||||
| border: 1px solid #fff; | |||||
| padding: 0; | |||||
| cursor: pointer; | |||||
| border-radius: 50%; | |||||
| font-size: 10px; | |||||
| line-height: 1; | |||||
| } | |||||
| .edgeButton:hover { | |||||
| box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08); | |||||
| } | |||||
| .edgeButtonDark { | |||||
| width: 14px; | |||||
| height: 14px; | |||||
| background: #0e0c0c; | |||||
| border: 1px solid #fff; | |||||
| padding: 0; | |||||
| cursor: pointer; | |||||
| border-radius: 50%; | |||||
| font-size: 10px; | |||||
| line-height: 1; | |||||
| } | |||||
| .edgeButtonDark:hover { | |||||
| box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08); | |||||
| } |
| } from '@xyflow/react'; | } from '@xyflow/react'; | ||||
| import useGraphStore from '../../store'; | import useGraphStore from '../../store'; | ||||
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { useFetchAgent } from '@/hooks/use-agent-request'; | import { useFetchAgent } from '@/hooks/use-agent-request'; | ||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import { NodeHandleId, Operator } from '../../constant'; | import { NodeHandleId, Operator } from '../../constant'; | ||||
| import styles from './index.less'; | |||||
| export function ButtonEdge({ | export function ButtonEdge({ | ||||
| id, | id, | ||||
| targetY, | targetY, | ||||
| targetPosition, | targetPosition, | ||||
| }); | }); | ||||
| const { theme } = useTheme(); | |||||
| const selectedStyle = useMemo(() => { | const selectedStyle = useMemo(() => { | ||||
| return selected ? { strokeWidth: 2, stroke: '#1677ff' } : {}; | |||||
| return selected ? { strokeWidth: 1, stroke: 'rgba(76, 164, 231, 1)' } : {}; | |||||
| }, [selected]); | }, [selected]); | ||||
| const onEdgeClick = () => { | const onEdgeClick = () => { | ||||
| // The set of elements following source | // The set of elements following source | ||||
| const slicedGraphPath = graphPath.slice(idx + 1); | const slicedGraphPath = graphPath.slice(idx + 1); | ||||
| if (slicedGraphPath.some((x) => x === target)) { | if (slicedGraphPath.some((x) => x === target)) { | ||||
| return { strokeWidth: 2, stroke: 'red' }; | |||||
| return { strokeWidth: 1, stroke: 'red' }; | |||||
| } | } | ||||
| } | } | ||||
| return {}; | return {}; | ||||
| path={edgePath} | path={edgePath} | ||||
| markerEnd={markerEnd} | markerEnd={markerEnd} | ||||
| style={{ ...style, ...selectedStyle, ...highlightStyle }} | style={{ ...style, ...selectedStyle, ...highlightStyle }} | ||||
| className="text-text-sub-title" | |||||
| /> | /> | ||||
| <EdgeLabelRenderer> | <EdgeLabelRenderer> | ||||
| > | > | ||||
| <button | <button | ||||
| className={cn( | className={cn( | ||||
| theme === 'dark' ? styles.edgeButtonDark : styles.edgeButton, | |||||
| 'size-3.5 border border-text-delete-red text-text-delete-red rounded-full leading-none', | |||||
| 'invisible', | 'invisible', | ||||
| { visible }, | { visible }, | ||||
| )} | )} |
| type: 'buttonEdge', | type: 'buttonEdge', | ||||
| markerEnd: 'logo', | markerEnd: 'logo', | ||||
| style: { | style: { | ||||
| strokeWidth: 2, | |||||
| stroke: 'rgb(202 197 245)', | |||||
| strokeWidth: 1, | |||||
| stroke: 'rgba(91, 93, 106, 1)', | |||||
| }, | }, | ||||
| zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498 | zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498 | ||||
| }} | }} |
| return ( | return ( | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | |||||
| <NodeWrapper selected={selected}> | |||||
| {isHeadAgent && ( | {isHeadAgent && ( | ||||
| <> | <> | ||||
| <CommonHandle | <CommonHandle |
| import { NodeWrapper } from './node-wrapper'; | 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({ data, id }: NodeProps<IBeginNode>) { | |||||
| function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {}); | const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {}); | ||||
| return ( | return ( | ||||
| <NodeWrapper> | |||||
| <NodeWrapper selected={selected}> | |||||
| <CommonHandle | <CommonHandle | ||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} |
| const { positions } = useBuildCategorizeHandlePositions({ data, id }); | const { positions } = useBuildCategorizeHandlePositions({ data, id }); | ||||
| return ( | return ( | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | |||||
| <NodeWrapper selected={selected}> | |||||
| <CommonHandle | <CommonHandle | ||||
| type="target" | type="target" | ||||
| position={Position.Left} | position={Position.Left} |
| }: NodeProps<IRagNode>) { | }: NodeProps<IRagNode>) { | ||||
| return ( | return ( | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | |||||
| <NodeWrapper selected={selected}> | |||||
| <CommonHandle | <CommonHandle | ||||
| id={NodeHandleId.End} | id={NodeHandleId.End} | ||||
| type="target" | type="target" |
| function InnerIterationStartNode({ | function InnerIterationStartNode({ | ||||
| isConnectable = true, | isConnectable = true, | ||||
| id, | id, | ||||
| selected, | |||||
| }: NodeProps<IIterationStartNode>) { | }: NodeProps<IIterationStartNode>) { | ||||
| return ( | return ( | ||||
| <NodeWrapper className="w-20"> | |||||
| <NodeWrapper className="w-20" selected={selected}> | |||||
| <CommonHandle | <CommonHandle | ||||
| type="source" | type="source" | ||||
| position={Position.Right} | position={Position.Right} |
| }: NodeProps<ILogicNode>) { | }: NodeProps<ILogicNode>) { | ||||
| return ( | return ( | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | |||||
| <NodeWrapper selected={selected}> | |||||
| <CommonHandle | <CommonHandle | ||||
| id="c" | id="c" | ||||
| type="source" | type="source" |
| const messages: string[] = get(data, 'form.messages', []); | const messages: string[] = get(data, 'form.messages', []); | ||||
| return ( | return ( | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | |||||
| <NodeWrapper selected={selected}> | |||||
| <CommonHandle | <CommonHandle | ||||
| type="target" | type="target" | ||||
| position={Position.Left} | position={Position.Left} |
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| import { HTMLAttributes, PropsWithChildren } from 'react'; | |||||
| import { HTMLAttributes } from 'react'; | |||||
| export function NodeWrapper({ | |||||
| children, | |||||
| className, | |||||
| }: PropsWithChildren & HTMLAttributes<HTMLDivElement>) { | |||||
| type IProps = HTMLAttributes<HTMLDivElement> & { selected?: boolean }; | |||||
| export function NodeWrapper({ children, className, selected }: IProps) { | |||||
| return ( | return ( | ||||
| <section | <section | ||||
| className={cn( | className={cn( | ||||
| 'bg-background-header-bar p-2.5 rounded-md w-[200px] text-xs', | 'bg-background-header-bar p-2.5 rounded-md w-[200px] text-xs', | ||||
| { 'border border-background-checked': selected }, | |||||
| className, | className, | ||||
| )} | )} | ||||
| > | > |
| text: z.string(), | text: z.string(), | ||||
| }); | }); | ||||
| function NoteNode({ data, id }: NodeProps<INoteNode>) { | |||||
| function NoteNode({ data, id, selected }: NodeProps<INoteNode>) { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const form = useForm<z.infer<typeof FormSchema>>({ | const form = useForm<z.infer<typeof FormSchema>>({ | ||||
| useWatchFormChange(id, form); | useWatchFormChange(id, form); | ||||
| return ( | return ( | ||||
| <NodeWrapper className="p-0 w-full h-full flex flex-col rounded-md "> | |||||
| <NodeWrapper | |||||
| className="p-0 w-full h-full flex flex-col rounded-md " | |||||
| selected={selected} | |||||
| > | |||||
| <NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}> | <NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}> | ||||
| <ResizeIcon /> | <ResizeIcon /> | ||||
| </NodeResizeControl> | </NodeResizeControl> |
| return ( | return ( | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | |||||
| <NodeWrapper selected={selected}> | |||||
| <CommonHandle | <CommonHandle | ||||
| id={NodeHandleId.End} | id={NodeHandleId.End} | ||||
| type="target" | type="target" |
| const { positions } = useBuildSwitchHandlePositions({ data, id }); | const { positions } = useBuildSwitchHandlePositions({ data, id }); | ||||
| return ( | return ( | ||||
| <ToolBar selected={selected} id={id} label={data.label}> | <ToolBar selected={selected} id={id} label={data.label}> | ||||
| <NodeWrapper> | |||||
| <NodeWrapper selected={selected}> | |||||
| <CommonHandle | <CommonHandle | ||||
| type="target" | type="target" | ||||
| position={Position.Left} | position={Position.Left} |
| import useGraphStore from '../../store'; | import useGraphStore from '../../store'; | ||||
| import { NodeWrapper } from './node-wrapper'; | import { NodeWrapper } from './node-wrapper'; | ||||
| function InnerToolNode({ id, isConnectable = true }: NodeProps<IToolNode>) { | |||||
| function InnerToolNode({ | |||||
| id, | |||||
| isConnectable = true, | |||||
| selected, | |||||
| }: NodeProps<IToolNode>) { | |||||
| const { edges, getNode } = useGraphStore((state) => state); | const { edges, getNode } = useGraphStore((state) => state); | ||||
| const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source; | const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source; | ||||
| const upstreamAgentNode = getNode(upstreamAgentNodeId); | const upstreamAgentNode = getNode(upstreamAgentNodeId); | ||||
| ); | ); | ||||
| return ( | return ( | ||||
| <NodeWrapper> | |||||
| <NodeWrapper selected={selected}> | |||||
| <Handle | <Handle | ||||
| id={NodeHandleId.End} | id={NodeHandleId.End} | ||||
| type="target" | type="target" |
| import { BeginQuery } from '../interface'; | import { BeginQuery } from '../interface'; | ||||
| import { FileUploadDirectUpload } from './uploader'; | import { FileUploadDirectUpload } from './uploader'; | ||||
| export const BeginQueryComponentMap = { | |||||
| [BeginQueryType.Line]: 'string', | |||||
| [BeginQueryType.Paragraph]: 'string', | |||||
| [BeginQueryType.Options]: 'string', | |||||
| [BeginQueryType.File]: 'file', | |||||
| [BeginQueryType.Integer]: 'number', | |||||
| [BeginQueryType.Boolean]: 'boolean', | |||||
| }; | |||||
| const StringFields = [ | const StringFields = [ | ||||
| BeginQueryType.Line, | BeginQueryType.Line, | ||||
| BeginQueryType.Paragraph, | BeginQueryType.Paragraph, | ||||
| } else if (type === BeginQueryType.Boolean) { | } else if (type === BeginQueryType.Boolean) { | ||||
| fieldSchema = z.boolean(); | fieldSchema = z.boolean(); | ||||
| value = false; | value = false; | ||||
| } else if (type === BeginQueryType.Integer) { | |||||
| } else if (type === BeginQueryType.Integer || type === 'float') { | |||||
| fieldSchema = z.coerce.number(); | fieldSchema = z.coerce.number(); | ||||
| } else { | } else { | ||||
| fieldSchema = z.record(z.any()); | fieldSchema = z.record(z.any()); |
| import CopyToClipboard from '@/components/copy-to-clipboard'; | import CopyToClipboard from '@/components/copy-to-clipboard'; | ||||
| import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet'; | import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet'; | ||||
| import { useDebugSingle } from '@/hooks/flow-hooks'; | |||||
| import { useFetchInputForm } from '@/hooks/use-agent-request'; | |||||
| import { useDebugSingle, useFetchInputForm } from '@/hooks/use-agent-request'; | |||||
| import { IModalProps } from '@/interfaces/common'; | import { IModalProps } from '@/interfaces/common'; | ||||
| import { cn } from '@/lib/utils'; | |||||
| import { isEmpty } from 'lodash'; | import { isEmpty } from 'lodash'; | ||||
| import { X } from 'lucide-react'; | import { X } from 'lucide-react'; | ||||
| import { useCallback, useMemo } from 'react'; | import { useCallback, useMemo } from 'react'; | ||||
| import JsonView from 'react18-json-view'; | import JsonView from 'react18-json-view'; | ||||
| import 'react18-json-view/src/style.css'; | import 'react18-json-view/src/style.css'; | ||||
| import DebugContent from '../../debug-content'; | import DebugContent from '../../debug-content'; | ||||
| import { transferInputsArrayToObject } from '../../form/begin-form/use-watch-change'; | |||||
| import { buildBeginInputListFromObject } from '../../form/begin-form/utils'; | import { buildBeginInputListFromObject } from '../../form/begin-form/utils'; | ||||
| interface IProps { | interface IProps { | ||||
| const onOk = useCallback( | const onOk = useCallback( | ||||
| (nextValues: any[]) => { | (nextValues: any[]) => { | ||||
| if (componentId) { | if (componentId) { | ||||
| debugSingle({ component_id: componentId, params: nextValues }); | |||||
| debugSingle({ | |||||
| component_id: componentId, | |||||
| params: transferInputsArrayToObject(nextValues), | |||||
| }); | |||||
| } | } | ||||
| }, | }, | ||||
| [componentId, debugSingle], | [componentId, debugSingle], | ||||
| submitButtonDisabled={list.length === 0} | submitButtonDisabled={list.length === 0} | ||||
| ></DebugContent> | ></DebugContent> | ||||
| {!isEmpty(data) ? ( | {!isEmpty(data) ? ( | ||||
| <div className="mt-4 rounded-md bg-slate-200 border border-neutral-200"> | |||||
| <div | |||||
| className={cn('mt-4 rounded-md border', { | |||||
| [`border-text-delete-red`]: !isEmpty(data._ERROR), | |||||
| })} | |||||
| > | |||||
| <div className="flex justify-between p-2"> | <div className="flex justify-between p-2"> | ||||
| <span>JSON</span> | <span>JSON</span> | ||||
| <CopyToClipboard text={content}></CopyToClipboard> | <CopyToClipboard text={content}></CopyToClipboard> | ||||
| src={data} | src={data} | ||||
| displaySize | displaySize | ||||
| collapseStringsAfterLength={100000000000} | collapseStringsAfterLength={100000000000} | ||||
| className="w-full h-[800px] break-words overflow-auto p-2 bg-slate-100" | |||||
| className="w-full h-[800px] break-words overflow-auto p-2" | |||||
| dark | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| ) : null} | ) : null} |