Feat: The bottom anchor of the agent node is only displayed when there is a downstream node #9869 (#10611)

### What problem does this PR solve?

Feat: The bottom anchor of the agent node is only displayed when there
is a downstream node #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-10-16 17:47:55 +08:00
committed by GitHub
parent e76db6e222
commit 70ffe2b4e8
7 changed files with 194 additions and 8 deletions

View File

@ -291,7 +291,7 @@ export const RAGFlowSelect = forwardRef<
onReset={handleReset} onReset={handleReset}
allowClear={allowClear} allowClear={allowClear}
ref={ref} ref={ref}
className={cn(triggerClassName, 'bg-bg-base')} className={cn('bg-bg-base', triggerClassName)}
> >
<SelectValue placeholder={placeholder}>{label}</SelectValue> <SelectValue placeholder={placeholder}>{label}</SelectValue>
</SelectTrigger> </SelectTrigger>

View File

@ -161,7 +161,7 @@ export type IIterationNode = BaseNode;
export type IIterationStartNode = BaseNode; export type IIterationStartNode = BaseNode;
export type IKeywordNode = BaseNode; export type IKeywordNode = BaseNode;
export type ICodeNode = BaseNode<ICodeForm>; export type ICodeNode = BaseNode<ICodeForm>;
export type IAgentNode = BaseNode; export type IAgentNode<T = any> = BaseNode<T>;
export type RAGFlowNodeType = export type RAGFlowNodeType =
| IBeginNode | IBeginNode

View File

@ -1,12 +1,14 @@
import LLMLabel from '@/components/llm-select/llm-label'; import LLMLabel from '@/components/llm-select/llm-label';
import { IAgentNode } from '@/interfaces/database/flow'; import { IAgentNode } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { Handle, NodeProps, Position } from '@xyflow/react'; import { Handle, NodeProps, Position } from '@xyflow/react';
import { get } from 'lodash'; import { get } from 'lodash';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AgentExceptionMethod, NodeHandleId } from '../../constant'; import { AgentExceptionMethod, NodeHandleId } from '../../constant';
import { AgentFormSchemaType } from '../../form/agent-form';
import useGraphStore from '../../store'; import useGraphStore from '../../store';
import { isBottomSubAgent } from '../../utils'; import { hasSubAgent, isBottomSubAgent } from '../../utils';
import { CommonHandle, LeftEndHandle } from './handle'; import { CommonHandle, LeftEndHandle } from './handle';
import { RightHandleStyle } from './handle-icon'; import { RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
@ -18,7 +20,7 @@ function InnerAgentNode({
data, data,
isConnectable = true, isConnectable = true,
selected, selected,
}: NodeProps<IAgentNode>) { }: NodeProps<IAgentNode<AgentFormSchemaType>>) {
const edges = useGraphStore((state) => state.edges); const edges = useGraphStore((state) => state.edges);
const { t } = useTranslation(); const { t } = useTranslation();
@ -30,6 +32,12 @@ function InnerAgentNode({
return get(data, 'form.exception_method'); return get(data, 'form.exception_method');
}, [data]); }, [data]);
const hasTools = useMemo(() => {
const tools = get(data, 'form.tools', []);
const mcp = get(data, 'form.mcp', []);
return tools.length > 0 || mcp.length > 0;
}, [data]);
const isGotoMethod = useMemo(() => { const isGotoMethod = useMemo(() => {
return exceptionMethod === AgentExceptionMethod.Goto; return exceptionMethod === AgentExceptionMethod.Goto;
}, [exceptionMethod]); }, [exceptionMethod]);
@ -51,7 +59,6 @@ function InnerAgentNode({
></CommonHandle> ></CommonHandle>
</> </>
)} )}
{isHeadAgent || ( {isHeadAgent || (
<Handle <Handle
type="target" type="target"
@ -67,7 +74,9 @@ function InnerAgentNode({
isConnectable={false} isConnectable={false}
id={NodeHandleId.AgentBottom} id={NodeHandleId.AgentBottom}
style={{ left: 180 }} style={{ left: 180 }}
className="!bg-accent-primary !size-2" className={cn('!bg-accent-primary !size-2 invisible', {
visible: hasSubAgent(edges, id),
})}
></Handle> ></Handle>
<Handle <Handle
type="source" type="source"
@ -75,7 +84,9 @@ function InnerAgentNode({
isConnectable={false} isConnectable={false}
id={NodeHandleId.Tool} id={NodeHandleId.Tool}
style={{ left: 20 }} style={{ left: 20 }}
className="!bg-accent-primary !size-2" className={cn('!bg-accent-primary !size-2 invisible', {
visible: hasTools,
})}
></Handle> ></Handle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<section className="flex flex-col gap-2"> <section className="flex flex-col gap-2">

View File

@ -69,6 +69,8 @@ const FormSchema = z.object({
cite: z.boolean().optional(), cite: z.boolean().optional(),
}); });
export type AgentFormSchemaType = z.infer<typeof FormSchema>;
const outputList = buildOutputList(initialAgentValues.outputs); const outputList = buildOutputList(initialAgentValues.outputs);
function AgentForm({ node }: INextOperatorForm) { function AgentForm({ node }: INextOperatorForm) {
@ -92,7 +94,7 @@ function AgentForm({ node }: INextOperatorForm) {
return isBottomSubAgent(edges, node?.id); return isBottomSubAgent(edges, node?.id);
}, [edges, node?.id]); }, [edges, node?.id]);
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<AgentFormSchemaType>({
defaultValues: defaultValues, defaultValues: defaultValues,
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
}); });

View File

@ -26,6 +26,7 @@ export function useBuildPromptExtraPromptOptions(
.map(([key, value]) => ({ .map(([key, value]) => ({
label: key, label: key,
value: wrapPromptWithTag(value, key), value: wrapPromptWithTag(value, key),
icon: null,
})) }))
.filter((x) => { .filter((x) => {
if (!has) { if (!has) {

View File

@ -162,6 +162,13 @@ export function hasSubAgentOrTool(edges: Edge[], nodeId?: string) {
return !!edge; return !!edge;
} }
export function hasSubAgent(edges: Edge[], nodeId?: string) {
const edge = edges.find(
(x) => x.source === nodeId && x.sourceHandle === NodeHandleId.AgentBottom,
);
return !!edge;
}
// construct a dsl based on the node information of the graph // construct a dsl based on the node information of the graph
export const buildDslComponentsByGraph = ( export const buildDslComponentsByGraph = (
nodes: RAGFlowNodeType[], nodes: RAGFlowNodeType[],

View File

@ -0,0 +1,165 @@
import { Form } from '@/components/ui/form';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { useForm } from 'react-hook-form';
import { NodeCollapsible } from '@/components/collapse';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Example/NodeCollapsible',
component: NodeCollapsible,
parameters: {
layout: 'centered',
docs: {
description: {
component: `
## Component Description
NodeCollapsible is a specialized component for displaying collapsible content within nodes.
It automatically shows only the first 3 items and provides a toggle button to expand/collapse the rest.
The component is designed to work within the application's node-based UI, such as in agent or data flow canvases.
The toggle button is displayed as a small circle at the bottom center of the component when there are more than 3 items.
`,
},
},
},
tags: ['autodocs'],
argTypes: {
items: {
control: 'object',
description: 'Array of items to display in the collapsible component',
},
children: {
control: false,
description: 'Function to render each item',
},
className: {
control: 'text',
description: 'Additional CSS classes to apply to the component',
},
},
} satisfies Meta<typeof NodeCollapsible>;
// Form wrapper decorator
const WithFormProvider = ({ children }: { children: React.ReactNode }) => {
const form = useForm({
defaultValues: {},
resolver: zodResolver(z.object({})),
});
return <Form {...form}>{children}</Form>;
};
const withFormProvider = (Story: any) => (
<WithFormProvider>
<Story />
</WithFormProvider>
);
export default meta;
type Story = StoryObj<typeof meta>;
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Default: Story = {
decorators: [withFormProvider],
args: {
items: [
'Document Analysis Parser',
'Web Search Parser',
'Database Query Parser',
'Image Recognition Parser',
'Audio Transcription Parser',
'Video Processing Parser',
'Code Analysis Parser',
'Spreadsheet Parser',
],
children: (item: string) => (
<div className="px-4 py-2 border rounded-md bg-bg-component">{item}</div>
),
},
parameters: {
docs: {
description: {
story: `
### Basic Usage
By default, the NodeCollapsible component shows the first 3 items and collapses the rest.
A toggle button appears at the bottom when there are more than 3 items.
\`\`\`tsx
import { NodeCollapsible } from '@/components/collapse';
<NodeCollapsible
items={[
'Document Analysis Parser',
'Web Search Parser',
'Database Query Parser',
'Image Recognition Parser',
'Audio Transcription Parser',
'Video Processing Parser',
'Code Analysis Parser',
'Spreadsheet Parser'
]}
>
{(item) => (
<div className="px-4 py-2 border rounded-md bg-bg-component">
{item}
</div>
)}
</NodeCollapsible>
\`\`\`
`,
},
},
},
};
export const WithFewItems: Story = {
decorators: [withFormProvider],
args: {
items: ['Single Item'],
children: (item: string) => (
<div className="px-4 py-2 border rounded-md bg-bg-component">{item}</div>
),
},
parameters: {
docs: {
description: {
story: `
When there are 3 or fewer items, no toggle button is shown.
`,
},
},
},
};
export const WithManyItems: Story = {
decorators: [withFormProvider],
args: {
items: [
'Item 1',
'Item 2',
'Item 3',
'Item 4',
'Item 5',
'Item 6',
'Item 7',
'Item 8',
],
children: (item: string) => (
<div className="px-4 py-2 border rounded-md bg-bg-component">{item}</div>
),
},
parameters: {
docs: {
description: {
story: `
When there are more than 3 items, a toggle button is shown at the bottom center.
Clicking it will expand to show all items.
`,
},
},
},
};