浏览代码

feat: Display input parameters on operator nodes #3240 (#3241)

### What problem does this PR solve?
feat: Display input parameters on operator nodes #3240


### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.14.0
balibabu 1年前
父节点
当前提交
d3bb5e9f3d
没有帐户链接到提交者的电子邮件

+ 37
- 0
web/package-lock.json 查看文件

"@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.1", "@radix-ui/react-icons": "^1.3.1",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-select": "^2.1.2", "@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
} }
} }
}, },
"node_modules/@radix-ui/react-popover": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
"integrity": "sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.1",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.0",
"@radix-ui/react-portal": "1.1.2",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.6.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": { "node_modules/@radix-ui/react-popper": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",

+ 1
- 0
web/package.json 查看文件

"@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.1", "@radix-ui/react-icons": "^1.3.1",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-select": "^2.1.2", "@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",

+ 31
- 0
web/src/components/ui/popover.tsx 查看文件

'use client';

import * as PopoverPrimitive from '@radix-ui/react-popover';
import * as React from 'react';

import { cn } from '@/lib/utils';

const Popover = PopoverPrimitive.Root;

const PopoverTrigger = PopoverPrimitive.Trigger;

const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverContent, PopoverTrigger };

+ 117
- 0
web/src/components/ui/table.tsx 查看文件

import * as React from 'react';

import { cn } from '@/lib/utils';

const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
));
Table.displayName = 'Table';

const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';

const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
));
TableBody.displayName = 'TableBody';

const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
className,
)}
{...props}
/>
));
TableFooter.displayName = 'TableFooter';

const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className,
)}
{...props}
/>
));
TableRow.displayName = 'TableRow';

const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
));
TableHead.displayName = 'TableHead';

const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
));
TableCell.displayName = 'TableCell';

const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props}
/>
));
TableCaption.displayName = 'TableCaption';

export {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
};

+ 2
- 1
web/src/interfaces/common.ts 查看文件



export interface IModalProps<T> { export interface IModalProps<T> {
showModal?(): void; showModal?(): void;
hideModal(): void;
hideModal?(): void;
switchVisible?(visible: boolean): void;
visible?: boolean; visible?: boolean;
loading?: boolean; loading?: boolean;
onOk?(payload?: T): Promise<any> | void; onOk?(payload?: T): Promise<any> | void;

+ 3
- 0
web/src/locales/en.ts 查看文件

'If the response is HTML formatted and only the primary content wanted, please toggle it on.', 'If the response is HTML formatted and only the primary content wanted, please toggle it on.',
reference: 'Reference', reference: 'Reference',
input: 'Input', input: 'Input',
output: 'Output',
parameter: 'Parameter', parameter: 'Parameter',
howUseId: 'How to use agent ID?', howUseId: 'How to use agent ID?',
content: 'Content',
operationResults: 'Operation Results',
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',

+ 3
- 0
web/src/locales/zh-traditional.ts 查看文件

cleanHtmlTip: '如果回應是 HTML 格式並且只需要主要內容,請將其開啟。', cleanHtmlTip: '如果回應是 HTML 格式並且只需要主要內容,請將其開啟。',
reference: '引用', reference: '引用',
input: '輸入', input: '輸入',
output: '輸出',
parameter: '參數', parameter: '參數',
howUseId: '如何使用Agent ID?', howUseId: '如何使用Agent ID?',
content: '內容',
operationResults: '運行結果',
}, },
footer: { footer: {
profile: '“保留所有權利 @ react”', profile: '“保留所有權利 @ react”',

+ 3
- 0
web/src/locales/zh.ts 查看文件

cleanHtmlTip: '如果响应是 HTML 格式且只需要主要内容,请将其打开。', cleanHtmlTip: '如果响应是 HTML 格式且只需要主要内容,请将其打开。',
reference: '引用', reference: '引用',
input: '输入', input: '输入',
output: '输出',
parameter: '参数', parameter: '参数',
howUseId: '如何使用Agent ID?', howUseId: '如何使用Agent ID?',
content: '内容',
operationResults: '运行结果',
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',

+ 57
- 0
web/src/pages/flow/canvas/node/card.tsx 查看文件

import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';

export function CardWithForm() {
return (
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Create project</CardTitle>
<CardDescription>Deploy your new project in one-click.</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Name of your project" />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="framework">Framework</Label>
<Select>
<SelectTrigger id="framework">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="next">Next.js</SelectItem>
<SelectItem value="sveltekit">SvelteKit</SelectItem>
<SelectItem value="astro">Astro</SelectItem>
<SelectItem value="nuxt">Nuxt.js</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</form>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
</Card>
);
}

+ 40
- 43
web/src/pages/flow/canvas/node/categorize-node.tsx 查看文件

import { useBuildCategorizeHandlePositions } from './hooks'; import { useBuildCategorizeHandlePositions } from './hooks';
import styles from './index.less'; import styles from './index.less';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
import NodePopover from './popover';


export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) { export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) {
const { positions } = useBuildCategorizeHandlePositions({ data, id }); const { positions } = useBuildCategorizeHandlePositions({ data, id });


return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
type="target"
position={Position.Left}
isConnectable
className={styles.handle}
id={'a'}
></Handle>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
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>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>


<Flex vertical gap={8}>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
{positions.map((position, idx) => {
return (
<div key={idx}>
<div className={styles.nodeText}>{position.text}</div>
<Handle
key={position.text}
id={position.text}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
style={{ ...RightHandleStyle, top: position.top }}
></Handle>
</div>
);
})}
</Flex>
</section>
</NodePopover>
<Flex vertical gap={8}>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
{positions.map((position, idx) => {
return (
<div key={idx}>
<div className={styles.nodeText}>{position.text}</div>
<Handle
key={position.text}
id={position.text}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
style={{ ...RightHandleStyle, top: position.top }}
></Handle>
</div>
);
})}
</Flex>
</section>
); );
} }

+ 46
- 49
web/src/pages/flow/canvas/node/generate-node.tsx 查看文件

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 NodePopover from './popover';


export function GenerateNode({ export function GenerateNode({
id, id,
const getLabel = useGetComponentLabelByValue(id); const getLabel = useGetComponentLabelByValue(id);


return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[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>
<section
className={classNames(styles.logicNode, {
[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={styles.nodeHeader}
></NodeHeader>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>


<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<Flex gap={8} vertical className={styles.generateParameters}>
{parameters.map((x) => (
<Flex
key={x.id}
align="center"
gap={6}
className={styles.conditionBlock}
>
<label htmlFor="">{x.key}</label>
<span className={styles.parameterValue}>
{getLabel(x.component_id)}
</span>
</Flex>
))}
</Flex>
</section>
</NodePopover>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<Flex gap={8} vertical className={styles.generateParameters}>
{parameters.map((x) => (
<Flex
key={x.id}
align="center"
gap={6}
className={styles.conditionBlock}
>
<label htmlFor="">{x.key}</label>
<span className={styles.parameterValue}>
{getLabel(x.component_id)}
</span>
</Flex>
))}
</Flex>
</section>
); );
} }

+ 23
- 26
web/src/pages/flow/canvas/node/index.tsx 查看文件

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 NodePopover from './popover';


export function RagNode({ export function RagNode({
id, id,
selected, selected,
}: NodeProps<NodeData>) { }: NodeProps<NodeData>) {
return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.ragNode, {
[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>
</NodePopover>
<section
className={classNames(styles.ragNode, {
[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>
); );
} }

+ 32
- 35
web/src/pages/flow/canvas/node/invoke-node.tsx 查看文件

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 NodePopover from './popover';


export function InvokeNode({ export function InvokeNode({
id, id,
const { t } = useTranslation(); const { t } = useTranslation();
const url = get(data, 'form.url'); const url = get(data, 'form.url');
return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.ragNode, {
[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}
className={styles.nodeHeader}
></NodeHeader>
<Flex vertical>
<div>{t('flow.url')}</div>
<div className={styles.nodeText}>{url}</div>
</Flex>
</section>
</NodePopover>
<section
className={classNames(styles.ragNode, {
[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}
className={styles.nodeHeader}
></NodeHeader>
<Flex vertical>
<div>{t('flow.url')}</div>
<div className={styles.nodeText}>{url}</div>
</Flex>
</section>
); );
} }

+ 31
- 34
web/src/pages/flow/canvas/node/keyword-node.tsx 查看文件

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 NodePopover from './popover';


export function KeywordNode({ export function KeywordNode({
id, id,
selected, selected,
}: NodeProps<NodeData>) { }: NodeProps<NodeData>) {
return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[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>
<section
className={classNames(styles.logicNode, {
[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={styles.nodeHeader}
></NodeHeader>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>


<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
</section>
</NodePopover>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
</section>
); );
} }

+ 23
- 26
web/src/pages/flow/canvas/node/logic-node.tsx 查看文件

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 NodePopover from './popover';


export function LogicNode({ export function LogicNode({
id, id,
selected, selected,
}: NodeProps<NodeData>) { }: NodeProps<NodeData>) {
return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[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}></NodeHeader>
</section>
</NodePopover>
<section
className={classNames(styles.logicNode, {
[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}></NodeHeader>
</section>
); );
} }

+ 38
- 41
web/src/pages/flow/canvas/node/message-node.tsx 查看文件

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 NodePopover from './popover';


export function MessageNode({ export function MessageNode({
id, id,
const messages: string[] = get(data, 'form.messages', []); const messages: string[] = get(data, 'form.messages', []);


return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
<section
className={classNames(styles.logicNode, {
[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,
})} })}
>
<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>
></NodeHeader>


<Flex vertical gap={8} className={styles.messageNodeContainer}>
{messages.map((message, idx) => {
return (
<div className={styles.nodeText} key={idx}>
{message}
</div>
);
})}
</Flex>
</section>
</NodePopover>
<Flex vertical gap={8} className={styles.messageNodeContainer}>
{messages.map((message, idx) => {
return (
<div className={styles.nodeText} key={idx}>
{message}
</div>
);
})}
</Flex>
</section>
); );
} }

+ 35
- 17
web/src/pages/flow/canvas/node/node-header.tsx 查看文件

import { Flex } from 'antd'; import { Flex } from 'antd';

import { Operator, operatorMap } from '../../constant'; import { Operator, operatorMap } from '../../constant';
import OperatorIcon from '../../operator-icon'; import OperatorIcon from '../../operator-icon';
import NodeDropdown from './dropdown'; import NodeDropdown from './dropdown';

import { useTranslate } from '@/hooks/common-hooks';
import styles from './index.less'; import styles from './index.less';
import { NextNodePopover } from './popover';


interface IProps { interface IProps {
id: string; id: string;
label: string;
name: string;
label?: string;
name?: string;
gap?: number; gap?: number;
className?: string; className?: string;
} }


export function RunStatus({ id, name }: IProps) {
const { t } = useTranslate('flow');
return (
<section className="flex justify-end items-center pb-1 ">
<NextNodePopover nodeId={id} name={name}>
<span className="text-blue-600 cursor-pointer text-[10px]">
{t('operationResults')}
</span>
</NextNodePopover>
</section>
);
}

const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => { const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => {
return ( return (
<Flex
flex={1}
align="center"
justify={'space-between'}
gap={gap}
className={className}
>
<OperatorIcon
name={label as Operator}
color={operatorMap[label as Operator].color}
></OperatorIcon>
<span className={styles.nodeTitle}>{name}</span>
<NodeDropdown id={id}></NodeDropdown>
</Flex>
<section className="haha">
{label !== Operator.Answer && <RunStatus id={id} name={name}></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>
<span className={styles.nodeTitle}>{name}</span>
<NodeDropdown id={id}></NodeDropdown>
</Flex>
</section>
); );
}; };



+ 77
- 28
web/src/pages/flow/canvas/node/popover.tsx 查看文件

import { useFetchFlow } from '@/hooks/flow-hooks'; import { useFetchFlow } from '@/hooks/flow-hooks';
import { Popover } from 'antd';
import get from 'lodash/get'; import get from 'lodash/get';
import React, { useMemo } from 'react';
import React, { MouseEventHandler, 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 { Operator } from '../../constant';
import { useReplaceIdWithText } from '../../hooks'; import { useReplaceIdWithText } from '../../hooks';


import styles from './index.less';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useTranslate } from '@/hooks/common-hooks';


interface IProps extends React.PropsWithChildren { interface IProps extends React.PropsWithChildren {
nodeId: string; nodeId: string;
name?: string;
} }


const NodePopover = ({ children, nodeId }: IProps) => {
export function NextNodePopover({ children, nodeId, name }: IProps) {
const { t } = useTranslate('flow');

const { data } = useFetchFlow(); const { data } = useFetchFlow();
const component = useMemo(() => { const component = useMemo(() => {
return get(data, ['dsl', 'components', nodeId], {}); return get(data, ['dsl', 'components', nodeId], {});
}, [nodeId, data]); }, [nodeId, data]);


const inputs: Array<{ component_id: string; content: string }> = get(
component,
['obj', 'params', 'inputs'],
[],
);
const output = get(component, ['obj', 'params', 'output'], {}); const output = get(component, ['obj', 'params', 'output'], {});
const componentName = get(component, ['obj', 'component_name'], '');
const replacedOutput = useReplaceIdWithText(output);

const content =
componentName !== Operator.Answer ? (
<div
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<JsonView
src={replacedOutput}
displaySize={30}
className={styles.jsonView}
/>
</div>
) : undefined;
const { replacedOutput, getNameById } = useReplaceIdWithText(output);
const stopPropagation: MouseEventHandler = useCallback((e) => {
e.stopPropagation();
}, []);


return ( return (
<Popover content={content} placement="right" destroyTooltipOnHide>
{children}
<Popover>
<PopoverTrigger onClick={stopPropagation} asChild>
{children}
</PopoverTrigger>
<PopoverContent
align={'start'}
side={'right'}
sideOffset={20}
onClick={stopPropagation}
className="w-[400px]"
>
<div className="mb-3 font-semibold text-[16px]">
{name} {t('operationResults')}
</div>
<div className="flex w-full gap-4 flex-col">
<div className="flex flex-col space-y-1.5">
<span className="font-semibold text-[14px]">{t('input')}</span>
<div className="bg-gray-100 p-1 rounded">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('componentId')}</TableHead>
<TableHead className="w-[60px]">{t('content')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{inputs.map((x, idx) => (
<TableRow key={idx}>
<TableCell>{getNameById(x.component_id)}</TableCell>
<TableCell className="truncate">{x.content}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
<div className="flex flex-col space-y-1.5">
<span className="font-semibold text-[14px]">{t('output')}</span>
<div className="bg-gray-100 p-1 rounded">
<JsonView
src={replacedOutput}
displaySize={30}
className="w-full max-h-[300px] break-words overflow-auto"
/>
</div>
</div>
</div>
</PopoverContent>
</Popover> </Popover>
); );
};

export default NodePopover;
}

+ 44
- 47
web/src/pages/flow/canvas/node/relevant-node.tsx 查看文件

import { Handle, NodeProps, Position } from 'reactflow'; import { Handle, NodeProps, Position } from 'reactflow';
import { NodeData } from '../../interface'; import { NodeData } from '../../interface';
import { RightHandleStyle } from './handle-icon'; import { RightHandleStyle } from './handle-icon';
import NodePopover from './popover';


import { get } from 'lodash'; import { get } from 'lodash';
import styles from './index.less'; import styles from './index.less';
const yes = get(data, 'form.yes'); const yes = get(data, 'form.yes');
const no = get(data, 'form.no'); const no = get(data, 'form.no');
return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
type="target"
position={Position.Left}
isConnectable
className={styles.handle}
id={'a'}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
id={'yes'}
style={{ ...RightHandleStyle, top: 59 }}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
id={'no'}
style={{ ...RightHandleStyle, top: 112 }}
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
type="target"
position={Position.Left}
isConnectable
className={styles.handle}
id={'a'}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
id={'yes'}
style={{ ...RightHandleStyle, top: 59 }}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
id={'no'}
style={{ ...RightHandleStyle, top: 112 }}
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>


<Flex vertical gap={10}>
<Flex vertical>
<div className={styles.relevantLabel}>Yes</div>
<div className={styles.nodeText}>{yes}</div>
</Flex>
<Flex vertical>
<div className={styles.relevantLabel}>No</div>
<div className={styles.nodeText}>{no}</div>
</Flex>
<Flex vertical gap={10}>
<Flex vertical>
<div className={styles.relevantLabel}>Yes</div>
<div className={styles.nodeText}>{yes}</div>
</Flex> </Flex>
</section>
</NodePopover>
<Flex vertical>
<div className={styles.relevantLabel}>No</div>
<div className={styles.nodeText}>{no}</div>
</Flex>
</Flex>
</section>
); );
} }

+ 46
- 49
web/src/pages/flow/canvas/node/retrieval-node.tsx 查看文件

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 NodePopover from './popover';


export function RetrievalNode({ export function RetrievalNode({
id, id,
}, [knowledgeList, knowledgeBaseIds]); }, [knowledgeList, knowledgeBaseIds]);


return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
<section
className={classNames(styles.logicNode, {
[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,
})} })}
>
<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}
</Flex>
></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>
</div>
);
})}
</Flex>
</section>
</NodePopover>
</Flex>
</div>
);
})}
</Flex>
</section>
); );
} }

+ 31
- 34
web/src/pages/flow/canvas/node/rewrite-node.tsx 查看文件

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 NodePopover from './popover';


export function RewriteNode({ export function RewriteNode({
id, id,
selected, selected,
}: NodeProps<NodeData>) { }: NodeProps<NodeData>) {
return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[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>
<section
className={classNames(styles.logicNode, {
[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={styles.nodeHeader}
></NodeHeader>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>


<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
</section>
</NodePopover>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
</section>
); );
} }

+ 47
- 50
web/src/pages/flow/canvas/node/switch-node.tsx 查看文件

import { useBuildSwitchHandlePositions } from './hooks'; import { useBuildSwitchHandlePositions } from './hooks';
import styles from './index.less'; import styles from './index.less';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
import NodePopover from './popover';


const getConditionKey = (idx: number, length: number) => { const getConditionKey = (idx: number, length: number) => {
if (idx === 0 && length !== 1) { if (idx === 0 && length !== 1) {
const { positions } = useBuildSwitchHandlePositions({ data, id }); const { positions } = useBuildSwitchHandlePositions({ data, id });


return ( return (
<NodePopover nodeId={id}>
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
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>
<Flex vertical gap={10}>
{positions.map((position, idx) => {
return (
<div key={idx}>
<Flex vertical>
<Flex justify={'space-between'}>
<span>{idx < positions.length - 1 && position.text}</span>
<span>{getConditionKey(idx, positions.length)}</span>
</Flex>
{position.condition && (
<ConditionBlock
nodeId={id}
condition={position.condition}
></ConditionBlock>
)}
<section
className={classNames(styles.logicNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
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>
<Flex vertical gap={10}>
{positions.map((position, idx) => {
return (
<div key={idx}>
<Flex vertical>
<Flex justify={'space-between'}>
<span>{idx < positions.length - 1 && position.text}</span>
<span>{getConditionKey(idx, positions.length)}</span>
</Flex> </Flex>
<Handle
key={position.text}
id={position.text}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
style={{ ...RightHandleStyle, top: position.top }}
></Handle>
</div>
);
})}
</Flex>
</section>
</NodePopover>
{position.condition && (
<ConditionBlock
nodeId={id}
condition={position.condition}
></ConditionBlock>
)}
</Flex>
<Handle
key={position.text}
id={position.text}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
style={{ ...RightHandleStyle, top: position.top }}
></Handle>
</div>
);
})}
</Flex>
</section>
); );
} }

+ 4
- 1
web/src/pages/flow/hooks.ts 查看文件

return getNode(id)?.data.name; return getNode(id)?.data.name;
}; };


return replaceIdWithText(output, getNameById);
return {
replacedOutput: replaceIdWithText(output, getNameById),
getNameById,
};
}; };


/** /**

正在加载...
取消
保存