Feat: Add loop operator node. #10427 (#11449)

### What problem does this PR solve?

Feat: Add loop operator node. #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-11-27 15:55:46 +08:00
committed by GitHub
parent b6314164c5
commit f57f32cf3a
51 changed files with 1246 additions and 1138 deletions

View File

@ -0,0 +1,18 @@
import { omit } from 'lodash';
import { Segmented, SegmentedProps } from './ui/segmented';
export function BoolSegmented({ ...props }: Omit<SegmentedProps, 'options'>) {
return (
<Segmented
options={
[
{ value: true, label: 'True' },
{ value: false, label: 'False' },
] as any
}
sizeType="sm"
itemClassName="justify-center flex-1"
{...omit(props, 'options')}
></Segmented>
);
}

View File

@ -0,0 +1,24 @@
import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options';
import { RAGFlowFormItem } from './ragflow-form';
import { RAGFlowSelect } from './ui/select';
type LogicalOperatorProps = { name: string };
export function LogicalOperator({ name }: LogicalOperatorProps) {
const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions();
return (
<div className="relative min-w-14">
<RAGFlowFormItem
name={name}
className="absolute top-1/2 -translate-y-1/2 right-1 left-0 z-10 bg-bg-base"
>
<RAGFlowSelect
options={switchLogicOperatorOptions}
triggerClassName="w-full text-xs px-1 py-0 h-6"
></RAGFlowSelect>
</RAGFlowFormItem>
<div className="absolute border-l border-y w-5 right-0 top-4 bottom-4 rounded-l-lg"></div>
</div>
);
}

View File

@ -17,15 +17,13 @@ import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { SwitchLogicOperator, SwitchOperatorOptions } from '@/constants/agent';
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options';
import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request';
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
import { Plus, X } from 'lucide-react';
import { useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { RAGFlowFormItem } from '../ragflow-form';
import { RAGFlowSelect } from '../ui/select';
import { LogicalOperator } from '../logical-operator';
export function MetadataFilterConditions({
kbIds,
@ -44,8 +42,6 @@ export function MetadataFilterConditions({
const switchOperatorOptions = useBuildSwitchOperatorOptions();
const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions();
const { fields, remove, append } = useFieldArray({
name,
control: form.control,
@ -53,14 +49,16 @@ export function MetadataFilterConditions({
const add = useCallback(
(key: string) => () => {
form.setValue(logic, SwitchLogicOperator.And);
if (fields.length === 1) {
form.setValue(logic, SwitchLogicOperator.And);
}
append({
key,
value: '',
op: SwitchOperatorOptions[0].value,
});
},
[append, form, logic],
[append, fields.length, form, logic],
);
return (
@ -85,20 +83,7 @@ export function MetadataFilterConditions({
</DropdownMenu>
</div>
<section className="flex">
{fields.length > 1 && (
<div className="relative min-w-14">
<RAGFlowFormItem
name={logic}
className="absolute top-1/2 -translate-y-1/2 right-1 left-0 z-10 bg-bg-base"
>
<RAGFlowSelect
options={switchLogicOperatorOptions}
triggerClassName="w-full text-xs px-1 py-0 h-6"
></RAGFlowSelect>
</RAGFlowFormItem>
<div className="absolute border-l border-y w-5 right-0 top-4 bottom-4 rounded-l-lg"></div>
</div>
)}
{fields.length > 1 && <LogicalOperator name={logic}></LogicalOperator>}
<div className="space-y-5 flex-1">
{fields.map((field, index) => {
const typeField = `${name}.${index}.key`;

View File

@ -75,7 +75,6 @@ export enum Operator {
Message = 'Message',
Relevant = 'Relevant',
RewriteQuestion = 'RewriteQuestion',
KeywordExtract = 'KeywordExtract',
DuckDuckGo = 'DuckDuckGo',
Wikipedia = 'Wikipedia',
PubMed = 'PubMed',
@ -84,14 +83,10 @@ export enum Operator {
Bing = 'Bing',
GoogleScholar = 'GoogleScholar',
GitHub = 'GitHub',
QWeather = 'QWeather',
ExeSQL = 'ExeSQL',
Switch = 'Switch',
WenCai = 'WenCai',
AkShare = 'AkShare',
YahooFinance = 'YahooFinance',
Jin10 = 'Jin10',
TuShare = 'TuShare',
Note = 'Note',
Crawler = 'Crawler',
Invoke = 'Invoke',
@ -118,6 +113,9 @@ export enum Operator {
Splitter = 'Splitter',
HierarchicalMerger = 'HierarchicalMerger',
Extractor = 'Extractor',
Loop = 'Loop',
LoopStart = 'LoopItem',
ExitLoop = 'ExitLoop',
}
export enum ComparisonOperator {

View File

@ -1170,8 +1170,13 @@ Example: Virtual Hosted Style`,
addField: 'Add option',
addMessage: 'Add message',
loop: 'Loop',
loopTip:
loopDescription:
'Loop is the upper limit of the number of loops of the current component, when the number of loops exceeds the value of loop, it means that the component can not complete the current task, please re-optimize agent',
exitLoop: 'Exit loop',
exitLoopDescription: `Equivalent to "break". This node has no configuration items. When the loop body reaches this node, the loop terminates.`,
loopVariables: 'Loop Variables',
maximumLoopCount: 'Maximum loop count',
loopTerminationCondition: 'Loop termination condition',
yes: 'Yes',
no: 'No',
key: 'Key',
@ -1655,9 +1660,8 @@ This delimiter is used to split the input text into several text pieces echo of
variableAssignerDescription:
'This component performs operations on Data objects, including extracting, filtering, and editing keys and values in the Data.',
variableAggregator: 'Variable aggregator',
variableAggregatorDescription: `This process aggregates variables from multiple branches into a single variable to achieve unified configuration for downstream nodes.
The variable aggregation node (originally the variable assignment node) is a crucial node in the workflow. It is responsible for integrating the output results of different branches, ensuring that regardless of which branch is executed, its result can be referenced and accessed through a unified variable. This is extremely useful in multi-branch scenarios, as it maps variables with the same function across different branches to a single output variable, avoiding redundant definitions in downstream nodes.`,
variableAggregatorDescription: `
This process aggregates variables from multiple branches into a single variable to achieve unified configuration for downstream nodes.`,
inputVariables: 'Input variables',
runningHintText: 'is running...🕞',
openingSwitch: 'Opening switch',
@ -1886,10 +1890,10 @@ Important structured information may include: names, dates, locations, events, k
overwrite: 'Overwritten By',
clear: 'Clear',
set: 'Set',
'+=': 'Add',
'-=': 'Subtract',
'*=': 'Multiply',
'/=': 'Divide',
add: 'Add',
subtract: 'Subtract',
multiply: 'Multiply',
divide: 'Divide',
append: 'Append',
extend: 'Extend',
removeFirst: 'Remove first',

View File

@ -1102,9 +1102,14 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
messageMsg: '请输入消息或删除此字段。',
addField: '新增字段',
addMessage: '新增消息',
loop: '循环上限',
loopTip:
loop: '循环',
loopDescription:
'loop为当前组件循环次数上限当循环次数超过loop的值时说明组件不能完成当前任务请重新优化agent',
exitLoop: '退出循环',
exitLoopDescription: `等同于 "break"。此节点没有配置项。当循环体到达此节点时,循环终止。`,
loopVariables: '循环变量',
maximumLoopCount: '最大循环次数',
loopTerminationCondition: '循环终止条件',
yes: '是',
no: '否',
key: '键',
@ -1499,7 +1504,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
contentTip: 'content: 邮件内容(可选)',
jsonUploadTypeErrorMessage: '请上传json文件',
jsonUploadContentErrorMessage: 'json 文件错误',
iteration: '循环',
iteration: '迭代',
iterationDescription: `该组件负责迭代生成新的内容,对列表对象执行多次步骤直至输出所有结果。`,
delimiterTip: `该分隔符用于将输入文本分割成几个文本片段,每个文本片段的回显将作为每次迭代的输入项。`,
delimiterOptions: {
@ -1545,8 +1550,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
variableAssignerDescription:
'此组件对数据对象执行操作,包括提取、筛选和编辑数据中的键和值。',
variableAggregator: '变量聚合',
variableAggregatorDescription: `将多路分支的变量聚合一个变量,以实现下游节点统一配置。
变量聚合节点(原变量赋值节点)是工作流程中的一个关键节点,它负责整合不同分支的输出结果,确保无论哪个分支被执行,其结果都能通过一个统一的变量来引用和访问。这在多分支的情况下非常有用,可将不同分支下相同作用的变量映射为一个输出变量,避免下游节点重复定义。`,
variableAggregatorDescription: `该过程将来自多个分支的变量聚合一个变量,以实现下游节点统一配置。`,
inputVariables: '输入变量',
addVariable: '新增变量',
runningHintText: '正在运行中...🕞',

View File

@ -56,12 +56,14 @@ import { BeginNode } from './node/begin-node';
import { CategorizeNode } from './node/categorize-node';
import { DataOperationsNode } from './node/data-operations-node';
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { ExitLoopNode } from './node/exit-loop-node';
import { ExtractorNode } from './node/extractor-node';
import { FileNode } from './node/file-node';
import { InvokeNode } from './node/invoke-node';
import { IterationNode, IterationStartNode } from './node/iteration-node';
import { KeywordNode } from './node/keyword-node';
import { ListOperationsNode } from './node/list-operations-node';
import { LoopNode, LoopStartNode } from './node/loop-node';
import { MessageNode } from './node/message-node';
import NoteNode from './node/note-node';
import ParserNode from './node/parser-node';
@ -105,6 +107,9 @@ export const nodeTypes: NodeTypes = {
listOperationsNode: ListOperationsNode,
variableAssignerNode: VariableAssignerNode,
variableAggregatorNode: VariableAggregatorNode,
loopNode: LoopNode,
loopStartNode: LoopStartNode,
exitLoopNode: ExitLoopNode,
};
const edgeTypes = {

View File

@ -1,58 +0,0 @@
import OperateDropdown from '@/components/operate-dropdown';
import { CopyOutlined } from '@ant-design/icons';
import { Flex, MenuProps } from 'antd';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Operator } from '../../constant';
import { useDuplicateNode } from '../../hooks';
import useGraphStore from '../../store';
interface IProps {
id: string;
iconFontColor?: string;
label: string;
}
const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
const { t } = useTranslation();
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
const deleteIterationNodeById = useGraphStore(
(store) => store.deleteIterationNodeById,
);
const deleteNode = useCallback(() => {
if (label === Operator.Iteration) {
deleteIterationNodeById(id);
} else {
deleteNodeById(id);
}
}, [label, deleteIterationNodeById, id, deleteNodeById]);
const duplicateNode = useDuplicateNode();
const items: MenuProps['items'] = [
{
key: '2',
onClick: () => duplicateNode(id, label),
label: (
<Flex justify={'space-between'}>
{t('common.copy')}
<CopyOutlined />
</Flex>
),
},
];
return (
<OperateDropdown
iconFontSize={22}
height={14}
deleteItem={deleteNode}
items={items}
needsDeletionValidation={false}
iconFontColor={iconFontColor}
></OperateDropdown>
);
};
export default NodeDropdown;

View File

@ -21,11 +21,23 @@ function OperatorAccordionTrigger({ children }: PropsWithChildren) {
export function AccordionOperators({
isCustomDropdown = false,
mousePosition,
nodeId,
}: {
isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number };
nodeId?: string;
}) {
const { t } = useTranslation();
const { getOperatorTypeFromId, getParentIdById } = useGraphStore(
(state) => state,
);
const exitLoopList = useMemo(() => {
if (getOperatorTypeFromId(getParentIdById(nodeId)) === Operator.Loop) {
return [Operator.ExitLoop];
}
return [];
}, [getOperatorTypeFromId, getParentIdById, nodeId]);
return (
<Accordion
@ -62,6 +74,8 @@ export function AccordionOperators({
operators={[
Operator.Switch,
Operator.Iteration,
Operator.Loop,
...exitLoopList,
Operator.Categorize,
]}
isCustomDropdown={isCustomDropdown}

View File

@ -73,6 +73,7 @@ export function InnerNextStepDropdown({
<AccordionOperators
isCustomDropdown={true}
mousePosition={position}
nodeId={nodeId}
></AccordionOperators>
)}
</OnNodeCreatedContext.Provider>
@ -101,9 +102,11 @@ export function InnerNextStepDropdown({
</DropdownMenuLabel>
<HideModalContext.Provider value={hideModal}>
{isPipeline ? (
<PipelineAccordionOperators></PipelineAccordionOperators>
<PipelineAccordionOperators
nodeId={nodeId}
></PipelineAccordionOperators>
) : (
<AccordionOperators></AccordionOperators>
<AccordionOperators nodeId={nodeId}></AccordionOperators>
)}
</HideModalContext.Provider>
</DropdownMenuContent>

View File

@ -0,0 +1,23 @@
import { BaseNode } from '@/interfaces/database/agent';
import { NodeProps } from '@xyflow/react';
import { LeftEndHandle } from './handle';
import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar';
export function ExitLoopNode({ id, data, selected }: NodeProps<BaseNode<any>>) {
return (
<ToolBar
selected={selected}
id={id}
label={data.label}
showRun={false}
showCopy={false}
>
<NodeWrapper selected={selected}>
<LeftEndHandle></LeftEndHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</NodeWrapper>
</ToolBar>
);
}

View File

@ -56,7 +56,7 @@ export function InnerIterationNode({
);
}
function InnerIterationStartNode({
export function InnerIterationStartNode({
isConnectable = true,
id,
selected,

View File

@ -0,0 +1,75 @@
import { Panel, type NodeProps, type PanelPosition } from '@xyflow/react';
import { type ComponentProps, type ReactNode } from 'react';
import { BaseNode } from '@/components/xyflow/base-node';
import { cn } from '@/lib/utils';
/* GROUP NODE Label ------------------------------------------------------- */
export type GroupNodeLabelProps = ComponentProps<'div'>;
export function GroupNodeLabel({
children,
className,
...props
}: GroupNodeLabelProps) {
return (
<div className="h-full w-full" {...props}>
<div
className={cn(
'text-card-foreground bg-secondary w-fit p-2 text-xs',
className,
)}
>
{children}
</div>
</div>
);
}
export type GroupNodeProps = Partial<NodeProps> & {
label?: ReactNode;
position?: PanelPosition;
};
/* GROUP NODE -------------------------------------------------------------- */
export function LabeledGroupNode({
label = '',
position,
...props
}: GroupNodeProps) {
const getLabelClassName = (position?: PanelPosition) => {
switch (position) {
case 'top-left':
return 'rounded-br-sm';
case 'top-center':
return 'rounded-b-sm';
case 'top-right':
return 'rounded-bl-sm';
case 'bottom-left':
return 'rounded-tr-sm';
case 'bottom-right':
return 'rounded-tl-sm';
case 'bottom-center':
return 'rounded-t-sm';
default:
return 'rounded-br-sm';
}
};
return (
<BaseNode
className="bg-opacity-50 h-full overflow-hidden rounded-sm"
{...props}
>
<Panel className="m-0 p-0" position={position}>
{label && (
<GroupNodeLabel className={getLabelClassName(position)}>
{label}
</GroupNodeLabel>
)}
</Panel>
</BaseNode>
);
}

View File

@ -0,0 +1,16 @@
import { BaseNode } from '@/interfaces/database/agent';
import { NodeProps } from '@xyflow/react';
import { memo } from 'react';
import { InnerIterationNode, InnerIterationStartNode } from './iteration-node';
export function InnerLoopNode({ ...props }: NodeProps<BaseNode<any>>) {
return <InnerIterationNode {...props}></InnerIterationNode>;
}
export const LoopNode = memo(InnerLoopNode);
export function InnerLoopStartNode({ ...props }: NodeProps<BaseNode<any>>) {
return <InnerIterationStartNode {...props}></InnerIterationStartNode>;
}
export const LoopStartNode = memo(InnerLoopStartNode);

View File

@ -23,7 +23,7 @@ function InnerRetrievalNode({
const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []);
const { list: knowledgeList } = useFetchKnowledgeList(true);
const { getLabel } = useGetVariableLabelOrTypeByValue(id);
const { getLabel } = useGetVariableLabelOrTypeByValue({ nodeId: id });
return (
<ToolBar selected={selected} id={id} label={data.label}>

View File

@ -27,7 +27,7 @@ const ConditionBlock = ({
nodeId,
}: { condition: ISwitchCondition } & { nodeId: string }) => {
const items = condition?.items ?? [];
const { getLabel } = useGetVariableLabelOrTypeByValue(nodeId);
const { getLabel } = useGetVariableLabelOrTypeByValue({ nodeId });
const renderOperatorIcon = useCallback((operator?: string) => {
const item = SwitchOperatorOptions.find((x) => x.value === operator);

View File

@ -58,7 +58,7 @@ export function ToolBar({
const deleteNode: MouseEventHandler<HTMLDivElement> = useCallback(
(e) => {
e.stopPropagation();
if (label === Operator.Iteration) {
if ([Operator.Iteration, Operator.Loop].includes(label as Operator)) {
deleteIterationNodeById(id);
} else {
deleteNodeById(id);

View File

@ -61,7 +61,6 @@ export const AgentOperatorList = [
Operator.Categorize,
Operator.Message,
Operator.RewriteQuestion,
Operator.KeywordExtract,
Operator.Switch,
Operator.Iteration,
Operator.WaitingDialogue,
@ -79,10 +78,6 @@ export const DataOperationsOperatorOptions = [
export const SwitchElseTo = 'end_cpn_ids';
const initialQueryBaseValues = {
query: [],
};
export const initialRetrievalValues = {
query: AgentGlobalsSysQueryWithBrace,
top_n: 8,
@ -139,11 +134,6 @@ export const initialMessageValues = {
content: [''],
};
export const initialKeywordExtractValues = {
...initialLlmBaseValues,
top_n: 3,
...initialQueryBaseValues,
};
export const initialDuckValues = {
top_n: 10,
channel: Channel.Text,
@ -275,14 +265,6 @@ export const initialGithubValues = {
},
};
export const initialQWeatherValues = {
web_apikey: 'xxx',
type: 'weather',
user_type: 'free',
time_period: 'now',
...initialQueryBaseValues,
};
export const initialExeSqlValues = {
sql: '',
db_type: 'mysql',
@ -331,8 +313,6 @@ export const initialWenCaiValues = {
},
};
export const initialAkShareValues = { top_n: 10, ...initialQueryBaseValues };
export const initialYahooFinanceValues = {
stock_code: '',
info: true,
@ -349,22 +329,6 @@ export const initialYahooFinanceValues = {
},
};
export const initialJin10Values = {
type: 'flash',
secret_key: 'xxx',
flash_type: '1',
contain: '',
filter: '',
...initialQueryBaseValues,
};
export const initialTuShareValues = {
token: 'xxx',
src: 'eastmoney',
start_date: '2024-01-01 09:00:00',
...initialQueryBaseValues,
};
export const initialNoteValues = {
text: '',
};
@ -624,6 +588,13 @@ export const initialVariableAssignerValues = {};
export const initialVariableAggregatorValues = { outputs: {}, groups: [] };
export const initialLoopValues = {
loop_variables: [],
loop_termination_condition: [],
maximum_loop_count: 10,
outputs: {},
};
export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 },
{ top: 8, right: 18 },
@ -659,11 +630,6 @@ export const RestrictedUpstreamMap = {
Operator.RewriteQuestion,
Operator.Relevant,
],
[Operator.KeywordExtract]: [
Operator.Begin,
Operator.Message,
Operator.Relevant,
],
[Operator.DuckDuckGo]: [Operator.Begin, Operator.Retrieval],
[Operator.Wikipedia]: [Operator.Begin, Operator.Retrieval],
[Operator.PubMed]: [Operator.Begin, Operator.Retrieval],
@ -672,15 +638,11 @@ export const RestrictedUpstreamMap = {
[Operator.Bing]: [Operator.Begin, Operator.Retrieval],
[Operator.GoogleScholar]: [Operator.Begin, Operator.Retrieval],
[Operator.GitHub]: [Operator.Begin, Operator.Retrieval],
[Operator.QWeather]: [Operator.Begin, Operator.Retrieval],
[Operator.SearXNG]: [Operator.Begin, Operator.Retrieval],
[Operator.ExeSQL]: [Operator.Begin],
[Operator.Switch]: [Operator.Begin],
[Operator.WenCai]: [Operator.Begin],
[Operator.AkShare]: [Operator.Begin],
[Operator.YahooFinance]: [Operator.Begin],
[Operator.Jin10]: [Operator.Begin],
[Operator.TuShare]: [Operator.Begin],
[Operator.Crawler]: [Operator.Begin],
[Operator.Note]: [],
[Operator.Invoke]: [Operator.Begin],
@ -706,6 +668,9 @@ export const RestrictedUpstreamMap = {
[Operator.Tokenizer]: [Operator.Begin],
[Operator.Extractor]: [Operator.Begin],
[Operator.File]: [Operator.Begin],
[Operator.Loop]: [Operator.Begin],
[Operator.LoopStart]: [Operator.Begin],
[Operator.ExitLoop]: [Operator.Begin],
};
export const NodeMap = {
@ -715,7 +680,6 @@ export const NodeMap = {
[Operator.Message]: 'messageNode',
[Operator.Relevant]: 'relevantNode',
[Operator.RewriteQuestion]: 'rewriteNode',
[Operator.KeywordExtract]: 'keywordNode',
[Operator.DuckDuckGo]: 'ragNode',
[Operator.Wikipedia]: 'ragNode',
[Operator.PubMed]: 'ragNode',
@ -724,15 +688,11 @@ export const NodeMap = {
[Operator.Bing]: 'ragNode',
[Operator.GoogleScholar]: 'ragNode',
[Operator.GitHub]: 'ragNode',
[Operator.QWeather]: 'ragNode',
[Operator.SearXNG]: 'ragNode',
[Operator.ExeSQL]: 'ragNode',
[Operator.Switch]: 'switchNode',
[Operator.WenCai]: 'ragNode',
[Operator.AkShare]: 'ragNode',
[Operator.YahooFinance]: 'ragNode',
[Operator.Jin10]: 'ragNode',
[Operator.TuShare]: 'ragNode',
[Operator.Note]: 'noteNode',
[Operator.Crawler]: 'ragNode',
[Operator.Invoke]: 'ragNode',
@ -758,6 +718,9 @@ export const NodeMap = {
[Operator.ListOperations]: 'listOperationsNode',
[Operator.VariableAssigner]: 'variableAssignerNode',
[Operator.VariableAggregator]: 'variableAggregatorNode',
[Operator.Loop]: 'loopNode',
[Operator.LoopStart]: 'loopStartNode',
[Operator.ExitLoop]: 'exitLoopNode',
};
export enum BeginQueryType {
@ -891,3 +854,82 @@ export const ArrayFields = [
TypesWithArray.ArrayString,
TypesWithArray.ArrayObject,
];
export enum InputMode {
Constant = 'constant',
Variable = 'variable',
}
export enum LoopTerminationComparisonOperator {
Contains = ComparisonOperator.Contains,
NotContains = ComparisonOperator.NotContains,
StartWith = ComparisonOperator.StartWith,
EndWith = ComparisonOperator.EndWith,
Is = 'is',
IsNot = 'is not',
}
export const LoopTerminationStringComparisonOperator = [
LoopTerminationComparisonOperator.Contains,
LoopTerminationComparisonOperator.NotContains,
LoopTerminationComparisonOperator.StartWith,
LoopTerminationComparisonOperator.EndWith,
LoopTerminationComparisonOperator.Is,
LoopTerminationComparisonOperator.IsNot,
ComparisonOperator.Empty,
ComparisonOperator.NotEmpty,
];
export const LoopTerminationBooleanComparisonOperator = [
LoopTerminationComparisonOperator.Is,
LoopTerminationComparisonOperator.IsNot,
ComparisonOperator.Empty,
ComparisonOperator.NotEmpty,
];
// object or object array
export const LoopTerminationObjectComparisonOperator = [
ComparisonOperator.Empty,
ComparisonOperator.NotEmpty,
];
// string array or number array
export const LoopTerminationStringArrayComparisonOperator = [
LoopTerminationComparisonOperator.Contains,
LoopTerminationComparisonOperator.NotContains,
ComparisonOperator.Empty,
ComparisonOperator.NotEmpty,
];
export const LoopTerminationBooleanArrayComparisonOperator = [
LoopTerminationComparisonOperator.Is,
LoopTerminationComparisonOperator.IsNot,
ComparisonOperator.Empty,
ComparisonOperator.NotEmpty,
];
export const LoopTerminationNumberComparisonOperator = [
ComparisonOperator.Equal,
ComparisonOperator.NotEqual,
ComparisonOperator.GreatThan,
ComparisonOperator.LessThan,
ComparisonOperator.GreatEqual,
ComparisonOperator.LessEqual,
ComparisonOperator.Empty,
ComparisonOperator.NotEmpty,
];
export const LoopTerminationStringComparisonOperatorMap = {
[TypesWithArray.String]: LoopTerminationStringComparisonOperator,
[TypesWithArray.Number]: LoopTerminationNumberComparisonOperator,
[TypesWithArray.Boolean]: LoopTerminationBooleanComparisonOperator,
[TypesWithArray.Object]: LoopTerminationObjectComparisonOperator,
[TypesWithArray.ArrayString]: LoopTerminationStringArrayComparisonOperator,
[TypesWithArray.ArrayNumber]: LoopTerminationStringArrayComparisonOperator,
[TypesWithArray.ArrayBoolean]: LoopTerminationBooleanArrayComparisonOperator,
[TypesWithArray.ArrayObject]: LoopTerminationObjectComparisonOperator,
};
export enum AgentVariableType {
Begin = 'begin',
Conversation = 'conversation',
}

View File

@ -1,6 +1,5 @@
import { Operator } from '../constant';
import AgentForm from '../form/agent-form';
import AkShareForm from '../form/akshare-form';
import ArXivForm from '../form/arxiv-form';
import BeginForm from '../form/begin-form';
import BingForm from '../form/bing-form';
@ -19,13 +18,11 @@ import HierarchicalMergerForm from '../form/hierarchical-merger-form';
import InvokeForm from '../form/invoke-form';
import IterationForm from '../form/iteration-form';
import IterationStartForm from '../form/iteration-start-from';
import Jin10Form from '../form/jin10-form';
import KeywordExtractForm from '../form/keyword-extract-form';
import ListOperationsForm from '../form/list-operations-form';
import LoopForm from '../form/loop-form';
import MessageForm from '../form/message-form';
import ParserForm from '../form/parser-form';
import PubMedForm from '../form/pubmed-form';
import QWeatherForm from '../form/qweather-form';
import RelevantForm from '../form/relevant-form';
import RetrievalForm from '../form/retrieval-form/next';
import RewriteQuestionForm from '../form/rewrite-question-form';
@ -37,7 +34,6 @@ import TavilyExtractForm from '../form/tavily-extract-form';
import TavilyForm from '../form/tavily-form';
import TokenizerForm from '../form/tokenizer-form';
import ToolForm from '../form/tool-form';
import TuShareForm from '../form/tushare-form';
import UserFillUpForm from '../form/user-fill-up-form';
import VariableAggregatorForm from '../form/variable-aggregator-form';
import VariableAssignerForm from '../form/variable-assigner-form';
@ -76,9 +72,6 @@ export const FormConfigMap = {
[Operator.DuckDuckGo]: {
component: DuckDuckGoForm,
},
[Operator.KeywordExtract]: {
component: KeywordExtractForm,
},
[Operator.Wikipedia]: {
component: WikipediaForm,
},
@ -100,9 +93,6 @@ export const FormConfigMap = {
[Operator.GitHub]: {
component: GithubForm,
},
[Operator.QWeather]: {
component: QWeatherForm,
},
[Operator.ExeSQL]: {
component: ExeSQLForm,
},
@ -112,18 +102,9 @@ export const FormConfigMap = {
[Operator.WenCai]: {
component: WenCaiForm,
},
[Operator.AkShare]: {
component: AkShareForm,
},
[Operator.YahooFinance]: {
component: YahooFinanceForm,
},
[Operator.Jin10]: {
component: Jin10Form,
},
[Operator.TuShare]: {
component: TuShareForm,
},
[Operator.Crawler]: {
component: CrawlerForm,
},
@ -191,8 +172,13 @@ export const FormConfigMap = {
[Operator.VariableAssigner]: {
component: VariableAssignerForm,
},
[Operator.VariableAggregator]: {
component: VariableAggregatorForm,
},
[Operator.Loop]: {
component: LoopForm,
},
[Operator.ExitLoop]: {
component: () => <></>,
},
};

View File

@ -1,22 +0,0 @@
import { TopNFormField } from '@/components/top-n-item';
import { Form } from '@/components/ui/form';
import { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
const AkShareForm = ({ form, node }: INextOperatorForm) => {
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNFormField max={99}></TopNFormField>
</form>
</Form>
);
};
export default AkShareForm;

View File

@ -1,127 +0,0 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Collapse, Flex, Form, Input, Select } from 'antd';
import { PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useBuildVariableOptions } from '../../hooks/use-get-begin-query';
import styles from './index.less';
interface IProps {
node?: RAGFlowNodeType;
}
enum VariableType {
Reference = 'reference',
Input = 'input',
}
const getVariableName = (type: string) =>
type === VariableType.Reference ? 'component_id' : 'value';
const DynamicVariableForm = ({ node }: IProps) => {
const { t } = useTranslation();
const valueOptions = useBuildVariableOptions(node?.id, node?.parentId);
const form = Form.useFormInstance();
const options = [
{ value: VariableType.Reference, label: t('flow.reference') },
{ value: VariableType.Input, label: t('flow.text') },
];
const handleTypeChange = useCallback(
(name: number) => () => {
setTimeout(() => {
form.setFieldValue(['query', name, 'component_id'], undefined);
form.setFieldValue(['query', name, 'value'], undefined);
}, 0);
},
[form],
);
return (
<Form.List name="query">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Flex key={key} gap={10} align={'baseline'}>
<Form.Item
{...restField}
name={[name, 'type']}
className={styles.variableType}
>
<Select
options={options}
onChange={handleTypeChange(name)}
></Select>
</Form.Item>
<Form.Item noStyle dependencies={[name, 'type']}>
{({ getFieldValue }) => {
const type = getFieldValue(['query', name, 'type']);
return (
<Form.Item
{...restField}
name={[name, getVariableName(type)]}
className={styles.variableValue}
>
{type === VariableType.Reference ? (
<Select
placeholder={t('common.pleaseSelect')}
options={valueOptions}
></Select>
) : (
<Input placeholder={t('common.pleaseInput')} />
)}
</Form.Item>
);
}}
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Flex>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add({ type: VariableType.Reference })}
block
icon={<PlusOutlined />}
className={styles.addButton}
>
{t('flow.addVariable')}
</Button>
</Form.Item>
</>
)}
</Form.List>
);
};
export function FormCollapse({
children,
title,
}: PropsWithChildren<{ title: string }>) {
return (
<Collapse
className={styles.dynamicInputVariable}
defaultActiveKey={['1']}
items={[
{
key: '1',
label: <span className={styles.title}>{title}</span>,
children,
},
]}
/>
);
}
const DynamicInputVariable = ({ node }: IProps) => {
const { t } = useTranslation();
return (
<FormCollapse title={t('flow.input')}>
<DynamicVariableForm node={node}></DynamicVariableForm>
</FormCollapse>
);
};
export default DynamicInputVariable;

View File

@ -1,135 +0,0 @@
'use client';
import { SideDown } from '@/assets/icon/next-icon';
import { Button } from '@/components/ui/button';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { Plus, Trash2 } from 'lucide-react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useBuildVariableOptions } from '../../hooks/use-get-begin-query';
interface IProps {
node?: RAGFlowNodeType;
}
enum VariableType {
Reference = 'reference',
Input = 'input',
}
const getVariableName = (type: string) =>
type === VariableType.Reference ? 'component_id' : 'value';
export function DynamicVariableForm({ node }: IProps) {
const { t } = useTranslation();
const form = useFormContext();
const { fields, remove, append } = useFieldArray({
name: 'query',
control: form.control,
});
const valueOptions = useBuildVariableOptions(node?.id, node?.parentId);
const options = [
{ value: VariableType.Reference, label: t('flow.reference') },
{ value: VariableType.Input, label: t('flow.text') },
];
return (
<div>
{fields.map((field, index) => {
const typeField = `query.${index}.type`;
const typeValue = form.watch(typeField);
return (
<div key={field.id} className="flex items-center gap-1">
<FormField
control={form.control}
name={typeField}
render={({ field }) => (
<FormItem className="w-2/5">
<FormDescription />
<FormControl>
<RAGFlowSelect
{...field}
placeholder={t('common.pleaseSelect')}
options={options}
onChange={(val) => {
field.onChange(val);
form.resetField(`query.${index}.value`);
form.resetField(`query.${index}.component_id`);
}}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`query.${index}.${getVariableName(typeValue)}`}
render={({ field }) => (
<FormItem className="flex-1">
<FormDescription />
<FormControl>
{typeValue === VariableType.Reference ? (
<RAGFlowSelect
placeholder={t('common.pleaseSelect')}
{...field}
options={valueOptions}
></RAGFlowSelect>
) : (
<Input placeholder={t('common.pleaseInput')} {...field} />
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Trash2
className="cursor-pointer mx-3 size-4 text-colors-text-functional-danger"
onClick={() => remove(index)}
/>
</div>
);
})}
<Button onClick={append} className="mt-4" variant={'outline'} size={'sm'}>
<Plus />
{t('flow.addVariable')}
</Button>
</div>
);
}
export function DynamicInputVariable({ node }: IProps) {
const { t } = useTranslation();
return (
<Collapsible defaultOpen className="group/collapsible">
<CollapsibleTrigger className="flex justify-between w-full pb-2">
<span className="font-bold text-2xl text-colors-text-neutral-strong">
{t('flow.input')}
</span>
<Button variant={'icon'} size={'icon'}>
<SideDown />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<DynamicVariableForm node={node}></DynamicVariableForm>
</CollapsibleContent>
</Collapsible>
);
}

View File

@ -192,7 +192,7 @@ export default function VariablePickerMenuPlugin({
const [queryString, setQueryString] = React.useState<string | null>('');
let options = useFilterQueryVariableOptionsByTypes(types);
let options = useFilterQueryVariableOptionsByTypes({ types });
if (baseOptions) {
options = baseOptions as typeof options;

View File

@ -20,7 +20,7 @@ export function QueryVariableList({
const form = useFormContext();
const name = 'query';
let options = useFilterQueryVariableOptionsByTypes(types);
let options = useFilterQueryVariableOptionsByTypes({ types });
const secondOptions = flatOptions(options);

View File

@ -9,7 +9,10 @@ import { ReactNode } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { JsonSchemaDataType } from '../../constant';
import { useFilterQueryVariableOptionsByTypes } from '../../hooks/use-get-begin-query';
import {
BuildQueryVariableOptions,
useFilterQueryVariableOptionsByTypes,
} from '../../hooks/use-get-begin-query';
import { GroupedSelectWithSecondaryMenu } from './select-with-secondary-menu';
type QueryVariableProps = {
@ -21,7 +24,7 @@ type QueryVariableProps = {
onChange?: (value: string) => void;
pureQuery?: boolean;
value?: string;
};
} & BuildQueryVariableOptions;
export function QueryVariable({
name = 'query',
@ -32,11 +35,17 @@ export function QueryVariable({
onChange,
pureQuery = false,
value,
nodeIds = [],
variablesExceptOperatorOutputs,
}: QueryVariableProps) {
const { t } = useTranslation();
const form = useFormContext();
const finalOptions = useFilterQueryVariableOptionsByTypes(types);
const finalOptions = useFilterQueryVariableOptionsByTypes({
types,
nodeIds,
variablesExceptOperatorOutputs,
});
const renderWidget = (
value?: string,

View File

@ -49,7 +49,7 @@ export function VariableTable({
nodeId,
}: IProps) {
const { t } = useTranslation();
const { getLabel } = useGetVariableLabelOrTypeByValue(nodeId!);
const { getLabel } = useGetVariableLabelOrTypeByValue({ nodeId: nodeId! });
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(

View File

@ -2,7 +2,6 @@
import { FormContainer } from '@/components/form-container';
import { KeyInput } from '@/components/key-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { BlockButton, Button } from '@/components/ui/button';
import {
FormControl,
@ -11,13 +10,17 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Separator } from '@/components/ui/separator';
import { Operator } from '@/constants/agent';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import { X } from 'lucide-react';
import { ReactNode, useCallback, useMemo } from 'react';
import { ReactNode } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useBuildSubNodeOutputOptions } from './use-build-options';
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
import useGraphStore from '../../store';
import { QueryVariable } from '../components/query-variable';
interface IProps {
node?: RAGFlowNodeType;
@ -26,28 +29,22 @@ interface IProps {
export function DynamicOutputForm({ node }: IProps) {
const { t } = useTranslation();
const form = useFormContext();
const options = useBuildSubNodeOutputOptions(node?.id);
const { nodes } = useGraphStore((state) => state);
const childNodeIds = nodes
.filter(
(x) =>
x.parentId === node?.id &&
x.data.label !== Operator.IterationStart &&
!isEmpty(x.data?.form?.outputs),
)
.map((x) => x.id);
const name = 'outputs';
const flatOptions = useMemo(() => {
return options.reduce<{ label: string; value: string; type: string }[]>(
(pre, cur) => {
pre.push(...cur.options);
return pre;
},
[],
);
}, [options]);
const findType = useCallback(
(val: string) => {
const type = flatOptions.find((x) => x.value === val)?.type;
if (type) {
return `Array<${type}>`;
}
},
[flatOptions],
);
const { getType } = useGetVariableLabelOrTypeByValue({
nodeIds: childNodeIds,
});
const { fields, remove, append } = useFieldArray({
name: name,
@ -77,25 +74,15 @@ export function DynamicOutputForm({ node }: IProps) {
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
<QueryVariable
name={`${name}.${index}.ref`}
render={({ field }) => (
<FormItem className="w-2/5">
<FormControl>
<SelectWithSearch
options={options}
{...field}
onChange={(val) => {
form.setValue(typeField, findType(val));
field.onChange(val);
}}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
hideLabel
className="w-2/5"
onChange={(val) => {
form.setValue(typeField, `Array<${getType(val)}>`);
}}
nodeIds={childNodeIds}
></QueryVariable>
<FormField
control={form.control}
name={typeField}

View File

@ -9,7 +9,6 @@ import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
import { DynamicOutput } from './dynamic-output';
import { DynamicVariables } from './dynamic-variables';
import { OutputArray } from './interface';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-form-change';
@ -53,7 +52,6 @@ function IterationForm({ node }: INextOperatorForm) {
name="items_ref"
types={ArrayFields as any[]}
></QueryVariable>
<DynamicVariables name="variables" label="Variables"></DynamicVariables>
<DynamicOutput node={node}></DynamicOutput>
<Output list={outputList}></Output>
</FormWrapper>

View File

@ -1,31 +0,0 @@
import { buildOutputOptions } from '@/utils/canvas-util';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { Operator } from '../../constant';
import useGraphStore from '../../store';
export function useBuildSubNodeOutputOptions(nodeId?: string) {
const { nodes } = useGraphStore((state) => state);
const nodeOutputOptions = useMemo(() => {
if (!nodeId) {
return [];
}
const subNodeWithOutputList = nodes.filter(
(x) =>
x.parentId === nodeId &&
x.data.label !== Operator.IterationStart &&
!isEmpty(x.data?.form?.outputs),
);
return subNodeWithOutputList.map((x) => ({
label: x.data.name,
value: x.id,
title: x.data.name,
options: buildOutputOptions(x.data.form.outputs, x.id),
}));
}, [nodeId, nodes]);
return nodeOutputOptions;
}

View File

@ -1,145 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { useMemo } from 'react';
import { IOperatorForm } from '../../interface';
import {
Jin10CalendarDatashapeOptions,
Jin10CalendarTypeOptions,
Jin10FlashTypeOptions,
Jin10SymbolsDatatypeOptions,
Jin10SymbolsTypeOptions,
Jin10TypeOptions,
} from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable';
const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const jin10TypeOptions = useMemo(() => {
return Jin10TypeOptions.map((x) => ({
value: x,
label: t(`jin10TypeOptions.${x}`),
}));
}, [t]);
const jin10FlashTypeOptions = useMemo(() => {
return Jin10FlashTypeOptions.map((x) => ({
value: x,
label: t(`jin10FlashTypeOptions.${x}`),
}));
}, [t]);
const jin10CalendarTypeOptions = useMemo(() => {
return Jin10CalendarTypeOptions.map((x) => ({
value: x,
label: t(`jin10CalendarTypeOptions.${x}`),
}));
}, [t]);
const jin10CalendarDatashapeOptions = useMemo(() => {
return Jin10CalendarDatashapeOptions.map((x) => ({
value: x,
label: t(`jin10CalendarDatashapeOptions.${x}`),
}));
}, [t]);
const jin10SymbolsTypeOptions = useMemo(() => {
return Jin10SymbolsTypeOptions.map((x) => ({
value: x,
label: t(`jin10SymbolsTypeOptions.${x}`),
}));
}, [t]);
const jin10SymbolsDatatypeOptions = useMemo(() => {
return Jin10SymbolsDatatypeOptions.map((x) => ({
value: x,
label: t(`jin10SymbolsDatatypeOptions.${x}`),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('type')} name={'type'} initialValue={'flash'}>
<Select options={jin10TypeOptions}></Select>
</Form.Item>
<Form.Item label={t('secretKey')} name={'secret_key'}>
<Input></Input>
</Form.Item>
<Form.Item noStyle dependencies={['type']}>
{({ getFieldValue }) => {
const type = getFieldValue('type');
switch (type) {
case 'flash':
return (
<>
<Form.Item label={t('flashType')} name={'flash_type'}>
<Select options={jin10FlashTypeOptions}></Select>
</Form.Item>
<Form.Item label={t('contain')} name={'contain'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('filter')} name={'filter'}>
<Input></Input>
</Form.Item>
</>
);
case 'calendar':
return (
<>
<Form.Item label={t('calendarType')} name={'calendar_type'}>
<Select options={jin10CalendarTypeOptions}></Select>
</Form.Item>
<Form.Item
label={t('calendarDatashape')}
name={'calendar_datashape'}
>
<Select options={jin10CalendarDatashapeOptions}></Select>
</Form.Item>
</>
);
case 'symbols':
return (
<>
<Form.Item label={t('symbolsType')} name={'symbols_type'}>
<Select options={jin10SymbolsTypeOptions}></Select>
</Form.Item>
<Form.Item
label={t('symbolsDatatype')}
name={'symbols_datatype'}
>
<Select options={jin10SymbolsDatatypeOptions}></Select>
</Form.Item>
</>
);
case 'news':
return (
<>
<Form.Item label={t('contain')} name={'contain'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('filter')} name={'filter'}>
<Input></Input>
</Form.Item>
</>
);
default:
return <></>;
}
}}
</Form.Item>
</Form>
);
};
export default Jin10Form;

View File

@ -1,48 +0,0 @@
import { NextLLMSelect } from '@/components/llm-select/next';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useTranslation } from 'react-i18next';
import { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
const KeywordExtractForm = ({ form, node }: INextOperatorForm) => {
const { t } = useTranslation();
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<FormField
control={form.control}
name="llm_id"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.modelTip')}>
{t('chat.model')}
</FormLabel>
<FormControl>
<NextLLMSelect {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<TopNFormField></TopNFormField>
</form>
</Form>
);
};
export default KeywordExtractForm;

View File

@ -1,33 +1,27 @@
import { BoolSegmented } from '@/components/bool-segmented';
import { KeyInput } from '@/components/key-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { useIsDarkTheme } from '@/components/theme-provider';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { buildOptions } from '@/utils/form';
import { Editor, loader } from '@monaco-editor/react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { X } from 'lucide-react';
import { ReactNode, useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { TypesWithArray } from '../../constant';
import { buildConversationVariableSelectOptions } from '../../utils';
import { InputMode, TypesWithArray } from '../../constant';
import {
InputModeOptions,
buildConversationVariableSelectOptions,
} from '../../utils';
import { DynamicFormHeader } from '../components/dynamic-fom-header';
import { QueryVariable } from '../components/query-variable';
import { useInitializeConditions } from './use-watch-form-change';
loader.config({ paths: { vs: '/vs' } });
enum InputMode {
Constant = 'constant',
Variable = 'variable',
}
const InputModeOptions = buildOptions(InputMode);
type SelectKeysProps = {
name: string;
label: ReactNode;
@ -35,42 +29,15 @@ type SelectKeysProps = {
keyField?: string;
valueField?: string;
operatorField?: string;
nodeId?: string;
};
type RadioGroupProps = React.ComponentProps<typeof RadioGroupPrimitive.Root>;
type RadioButtonProps = Partial<
Omit<RadioGroupProps, 'onValueChange'> & {
onChange: RadioGroupProps['onValueChange'];
}
>;
function RadioButton({ value, onChange }: RadioButtonProps) {
return (
<RadioGroup
defaultValue="yes"
className="flex"
value={value}
onValueChange={onChange}
>
<div className="flex items-center gap-3">
<RadioGroupItem value="yes" id="r1" />
<Label htmlFor="r1">Yes</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="no" id="r2" />
<Label htmlFor="r2">No</Label>
</div>
</RadioGroup>
);
}
const VariableTypeOptions = buildConversationVariableSelectOptions();
const modeField = 'mode';
const modeField = 'input_mode';
const ConstantValueMap = {
[TypesWithArray.Boolean]: 'yes',
[TypesWithArray.Boolean]: true,
[TypesWithArray.Number]: 0,
[TypesWithArray.String]: '',
[TypesWithArray.ArrayBoolean]: '[]',
@ -85,8 +52,9 @@ export function DynamicVariables({
label,
tooltip,
keyField = 'variable',
valueField = 'parameter',
operatorField = 'operator',
valueField = 'value',
operatorField = 'type',
nodeId,
}: SelectKeysProps) {
const form = useFormContext();
const isDarkTheme = useIsDarkTheme();
@ -96,6 +64,9 @@ export function DynamicVariables({
control: form.control,
});
const { initializeVariableRelatedConditions } =
useInitializeConditions(nodeId);
const initializeValue = useCallback(
(mode: string, variableType: string, valueFieldAlias: string) => {
if (mode === InputMode.Variable) {
@ -112,23 +83,27 @@ export function DynamicVariables({
(mode: string, valueFieldAlias: string, operatorFieldAlias: string) => {
const variableType = form.getValues(operatorFieldAlias);
initializeValue(mode, variableType, valueFieldAlias);
// if (mode === InputMode.Variable) {
// form.setValue(valueFieldAlias, '');
// } else {
// const val = ConstantValueMap[variableType as TypesWithArray];
// form.setValue(valueFieldAlias, val);
// }
},
[form, initializeValue],
);
const handleVariableTypeChange = useCallback(
(variableType: string, valueFieldAlias: string, modeFieldAlias: string) => {
(
variableType: string,
valueFieldAlias: string,
modeFieldAlias: string,
keyFieldAlias: string,
) => {
const mode = form.getValues(modeFieldAlias);
initializeVariableRelatedConditions(
form.getValues(keyFieldAlias),
variableType,
);
initializeValue(mode, variableType, valueFieldAlias);
},
[form, initializeValue],
[form, initializeValue, initializeVariableRelatedConditions],
);
const renderParameter = useCallback(
@ -138,7 +113,7 @@ export function DynamicVariables({
if (mode === InputMode.Constant) {
if (logicalOperator === TypesWithArray.Boolean) {
return <RadioButton></RadioButton>;
return <BoolSegmented></BoolSegmented>;
}
if (logicalOperator === TypesWithArray.Number) {
@ -211,6 +186,7 @@ export function DynamicVariables({
val,
valueFieldAlias,
modeFieldAlias,
keyFieldAlias,
);
field.onChange(val);
}}

View File

@ -0,0 +1,52 @@
import { SliderInputFormField } from '@/components/slider-input-form-field';
import { Form } from '@/components/ui/form';
import { FormLayout } from '@/constants/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { initialLoopValues } from '../../constant';
import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { DynamicVariables } from './dynamic-variables';
import { LoopTerminationCondition } from './loop-termination-condition';
import { FormSchema, LoopFormSchemaType } from './schema';
import { useFormValues } from './use-values';
import { useWatchFormChange } from './use-watch-form-change';
function LoopForm({ node }: INextOperatorForm) {
const defaultValues = useFormValues(initialLoopValues, node);
const { t } = useTranslation();
const form = useForm<LoopFormSchemaType>({
defaultValues: defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<DynamicVariables
name="loop_variables"
label={t('flow.loopVariables')}
nodeId={node?.id}
></DynamicVariables>
<LoopTerminationCondition
label={t('flow.loopTerminationCondition')}
nodeId={node?.id}
></LoopTerminationCondition>
<SliderInputFormField
min={1}
max={100}
name="maximum_loop_count"
label={t('flow.maximumLoopCount')}
layout={FormLayout.Vertical}
></SliderInputFormField>
</FormWrapper>
</Form>
);
}
export default memo(LoopForm);

View File

@ -0,0 +1,316 @@
import { BoolSegmented } from '@/components/bool-segmented';
import { LogicalOperator } from '@/components/logical-operator';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { ComparisonOperator, SwitchLogicOperator } from '@/constants/agent';
import { loader } from '@monaco-editor/react';
import { toLower } from 'lodash';
import { X } from 'lucide-react';
import { ReactNode, useCallback, useMemo } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import {
AgentVariableType,
InputMode,
JsonSchemaDataType,
} from '../../constant';
import { useFilterChildNodeIds } from '../../hooks/use-filter-child-node-ids';
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
import { InputModeOptions } from '../../utils';
import { DynamicFormHeader } from '../components/dynamic-fom-header';
import { QueryVariable } from '../components/query-variable';
import { LoopFormSchemaType } from './schema';
import { useBuildLogicalOptions } from './use-build-logical-options';
import {
ConditionKeyType,
ConditionModeType,
ConditionOperatorType,
ConditionValueType,
useInitializeConditions,
} from './use-watch-form-change';
loader.config({ paths: { vs: '/vs' } });
const VariablesExceptOperatorOutputs = [AgentVariableType.Conversation];
type LoopTerminationConditionProps = {
label: ReactNode;
tooltip?: string;
keyField?: string;
valueField?: string;
operatorField?: string;
modeField?: string;
nodeId?: string;
};
const EmptyFields = [ComparisonOperator.Empty, ComparisonOperator.NotEmpty];
const LogicalOperatorFieldName = 'logical_operator';
const name = 'loop_termination_condition';
export function LoopTerminationCondition({
label,
tooltip,
keyField = 'variable',
valueField = 'value',
operatorField = 'operator',
modeField = 'input_mode',
nodeId,
}: LoopTerminationConditionProps) {
const form = useFormContext<LoopFormSchemaType>();
const childNodeIds = useFilterChildNodeIds(nodeId);
const nodeIds = useMemo(() => {
if (!nodeId) return [];
return [nodeId, ...childNodeIds];
}, [childNodeIds, nodeId]);
const { getType } = useGetVariableLabelOrTypeByValue({
nodeIds: nodeIds,
variablesExceptOperatorOutputs: VariablesExceptOperatorOutputs,
});
const {
initializeConditionMode,
initializeConditionOperator,
initializeConditionValue,
} = useInitializeConditions(nodeId);
const { fields, remove, append } = useFieldArray({
name: name,
control: form.control,
});
const { buildLogicalOptions } = useBuildLogicalOptions();
const getVariableType = useCallback(
(keyFieldName: ConditionKeyType) => {
const key = form.getValues(keyFieldName);
return toLower(getType(key));
},
[form, getType],
);
const initializeMode = useCallback(
(modeFieldAlias: ConditionModeType, keyFieldAlias: ConditionKeyType) => {
const keyType = getVariableType(keyFieldAlias);
initializeConditionMode(modeFieldAlias, keyType);
},
[getVariableType, initializeConditionMode],
);
const initializeValue = useCallback(
(valueFieldAlias: ConditionValueType, keyFieldAlias: ConditionKeyType) => {
const keyType = getVariableType(keyFieldAlias);
initializeConditionValue(valueFieldAlias, keyType);
},
[getVariableType, initializeConditionValue],
);
const handleVariableChange = useCallback(
(
operatorFieldAlias: ConditionOperatorType,
valueFieldAlias: ConditionValueType,
keyFieldAlias: ConditionKeyType,
modeFieldAlias: ConditionModeType,
) => {
return () => {
initializeConditionOperator(
operatorFieldAlias,
getVariableType(keyFieldAlias),
);
initializeMode(modeFieldAlias, keyFieldAlias);
initializeValue(valueFieldAlias, keyFieldAlias);
};
},
[
getVariableType,
initializeConditionOperator,
initializeMode,
initializeValue,
],
);
const handleOperatorChange = useCallback(
(
valueFieldAlias: ConditionValueType,
keyFieldAlias: ConditionKeyType,
modeFieldAlias: ConditionModeType,
) => {
initializeMode(modeFieldAlias, keyFieldAlias);
initializeValue(valueFieldAlias, keyFieldAlias);
},
[initializeMode, initializeValue],
);
const handleModeChange = useCallback(
(mode: string, valueFieldAlias: ConditionValueType) => {
form.setValue(valueFieldAlias, mode === InputMode.Constant ? 0 : '', {
shouldDirty: true,
});
},
[form],
);
const renderParameterPanel = useCallback(
(
keyFieldName: ConditionKeyType,
valueFieldAlias: ConditionValueType,
modeFieldAlias: ConditionModeType,
operatorFieldAlias: ConditionOperatorType,
) => {
const type = getVariableType(keyFieldName);
const mode = form.getValues(modeFieldAlias);
const operator = form.getValues(operatorFieldAlias);
if (EmptyFields.includes(operator as ComparisonOperator)) {
return null;
}
if (type === JsonSchemaDataType.Number) {
return (
<section className="flex items-center gap-1">
<RAGFlowFormItem name={modeFieldAlias}>
{(field) => (
<SelectWithSearch
value={field.value}
onChange={(val) => {
handleModeChange(val, valueFieldAlias);
field.onChange(val);
}}
options={InputModeOptions}
></SelectWithSearch>
)}
</RAGFlowFormItem>
<Separator className="w-2" />
{mode === InputMode.Constant ? (
<RAGFlowFormItem name={valueFieldAlias}>
<Input type="number" />
</RAGFlowFormItem>
) : (
<QueryVariable
types={[JsonSchemaDataType.Number]}
hideLabel
pureQuery
name={valueFieldAlias}
className="flex-1 min-w-0"
></QueryVariable>
)}
</section>
);
}
if (type === JsonSchemaDataType.Boolean) {
return (
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
<BoolSegmented></BoolSegmented>
</RAGFlowFormItem>
);
}
return (
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
<Input />
</RAGFlowFormItem>
);
},
[form, getVariableType, handleModeChange],
);
return (
<section className="space-y-2">
<DynamicFormHeader
label={label}
tooltip={tooltip}
onClick={() => {
if (fields.length === 1) {
form.setValue(LogicalOperatorFieldName, SwitchLogicOperator.And);
}
append({ [keyField]: '', [valueField]: '' });
}}
></DynamicFormHeader>
<section className="flex">
{fields.length > 1 && (
<LogicalOperator name={LogicalOperatorFieldName}></LogicalOperator>
)}
<div className="space-y-5 flex-1 min-w-0">
{fields.map((field, index) => {
const keyFieldAlias =
`${name}.${index}.${keyField}` as ConditionKeyType;
const valueFieldAlias =
`${name}.${index}.${valueField}` as ConditionValueType;
const operatorFieldAlias =
`${name}.${index}.${operatorField}` as ConditionOperatorType;
const modeFieldAlias =
`${name}.${index}.${modeField}` as ConditionModeType;
return (
<section key={field.id} className="flex gap-2">
<div className="flex-1 space-y-3 min-w-0">
<div className="flex items-center">
<QueryVariable
name={keyFieldAlias}
hideLabel
className="flex-1 min-w-0"
onChange={handleVariableChange(
operatorFieldAlias,
valueFieldAlias,
keyFieldAlias,
modeFieldAlias,
)}
nodeIds={nodeIds}
variablesExceptOperatorOutputs={
VariablesExceptOperatorOutputs
}
></QueryVariable>
<Separator className="w-2" />
<RAGFlowFormItem
name={operatorFieldAlias}
className="w-1/3"
>
{({ onChange, value }) => (
<SelectWithSearch
value={value}
onChange={(val) => {
handleOperatorChange(
valueFieldAlias,
keyFieldAlias,
modeFieldAlias,
);
onChange(val);
}}
options={buildLogicalOptions(
getVariableType(keyFieldAlias),
)}
></SelectWithSearch>
)}
</RAGFlowFormItem>
</div>
{renderParameterPanel(
keyFieldAlias,
valueFieldAlias,
modeFieldAlias,
operatorFieldAlias,
)}
</div>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X />
</Button>
</section>
);
})}
</div>
</section>
</section>
);
}

View File

@ -0,0 +1,24 @@
import { z } from 'zod';
export const FormSchema = z.object({
loop_variables: z.array(
z.object({
variable: z.string().optional(),
type: z.string().optional(),
value: z.string().or(z.number()).or(z.boolean()).optional(),
input_mode: z.string(),
}),
),
logical_operator: z.string(),
loop_termination_condition: z.array(
z.object({
variable: z.string().optional(),
operator: z.string().optional(),
value: z.string().or(z.number()).or(z.boolean()).optional(),
input_mode: z.string().optional(),
}),
),
maximum_loop_count: z.number(),
});
export type LoopFormSchemaType = z.infer<typeof FormSchema>;

View File

@ -0,0 +1,27 @@
import { SwitchOperatorOptions } from '@/constants/agent';
import { camelCase, toLower } from 'lodash';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { LoopTerminationStringComparisonOperatorMap } from '../../constant';
export function useBuildLogicalOptions() {
const { t } = useTranslation();
const buildLogicalOptions = useCallback(
(type: string) => {
return LoopTerminationStringComparisonOperatorMap[
toLower(type) as keyof typeof LoopTerminationStringComparisonOperatorMap
]?.map((x) => ({
label: t(
`flow.switchOperatorOptions.${camelCase(SwitchOperatorOptions.find((y) => y.value === x)?.label || x)}`,
),
value: x,
}));
},
[t],
);
return {
buildLogicalOptions,
};
}

View File

@ -0,0 +1,20 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty, omit } from 'lodash';
import { useMemo } from 'react';
export function useFormValues(
defaultValues: Record<string, any>,
node?: RAGFlowNodeType,
) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return omit(defaultValues, 'outputs');
}
return omit(formData, 'outputs');
}, [defaultValues, node?.data?.form]);
return values;
}

View File

@ -0,0 +1,116 @@
import { JsonSchemaDataType } from '@/constants/agent';
import { buildVariableValue } from '@/utils/canvas-util';
import { useCallback, useEffect } from 'react';
import { UseFormReturn, useFormContext, useWatch } from 'react-hook-form';
import { InputMode } from '../../constant';
import { IOutputs } from '../../interface';
import useGraphStore from '../../store';
import { LoopFormSchemaType } from './schema';
import { useBuildLogicalOptions } from './use-build-logical-options';
export function useWatchFormChange(
id?: string,
form?: UseFormReturn<LoopFormSchemaType>,
) {
let values = useWatch({ control: form?.control });
const { replaceNodeForm } = useGraphStore((state) => state);
useEffect(() => {
if (id) {
let nextValues = {
...values,
outputs: values.loop_variables?.reduce((pre, cur) => {
const variable = cur.variable;
if (variable) {
pre[variable] = {
type: cur.type,
value: '',
};
}
return pre;
}, {} as IOutputs),
};
replaceNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, replaceNodeForm, values]);
}
type ConditionPrefixType = `loop_termination_condition.${number}.`;
export type ConditionKeyType = `${ConditionPrefixType}variable`;
export type ConditionModeType = `${ConditionPrefixType}input_mode`;
export type ConditionValueType = `${ConditionPrefixType}value`;
export type ConditionOperatorType = `${ConditionPrefixType}operator`;
export function useInitializeConditions(id?: string) {
const form = useFormContext<LoopFormSchemaType>();
const { buildLogicalOptions } = useBuildLogicalOptions();
const initializeConditionMode = useCallback(
(modeFieldAlias: ConditionModeType, keyType: string) => {
if (keyType === JsonSchemaDataType.Number) {
form.setValue(modeFieldAlias, InputMode.Constant, {
shouldDirty: true,
shouldValidate: true,
});
}
},
[form],
);
const initializeConditionValue = useCallback(
(valueFieldAlias: ConditionValueType, keyType: string) => {
let initialValue: string | boolean | number = '';
if (keyType === JsonSchemaDataType.Number) {
initialValue = 0;
} else if (keyType === JsonSchemaDataType.Boolean) {
initialValue = true;
}
form.setValue(valueFieldAlias, initialValue, {
shouldDirty: true,
shouldValidate: true,
});
},
[form],
);
const initializeConditionOperator = useCallback(
(operatorFieldAlias: ConditionOperatorType, keyType: string) => {
const logicalOptions = buildLogicalOptions(keyType);
form.setValue(operatorFieldAlias, logicalOptions?.at(0)?.value, {
shouldDirty: true,
shouldValidate: true,
});
},
[buildLogicalOptions, form],
);
const initializeVariableRelatedConditions = useCallback(
(variable: string, variableType: string) => {
form?.getValues('loop_termination_condition').forEach((x, idx) => {
if (variable && x.variable === buildVariableValue(variable, id)) {
const prefix: ConditionPrefixType = `loop_termination_condition.${idx}.`;
initializeConditionMode(`${prefix}input_mode`, variableType);
initializeConditionValue(`${prefix}value`, variableType);
initializeConditionOperator(`${prefix}operator`, variableType);
}
});
},
[
form,
id,
initializeConditionMode,
initializeConditionOperator,
initializeConditionValue,
],
);
return {
initializeVariableRelatedConditions,
initializeConditionMode,
initializeConditionValue,
initializeConditionOperator,
};
}

View File

@ -1,157 +0,0 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { INextOperatorForm } from '../../interface';
import {
QWeatherLangOptions,
QWeatherTimePeriodOptions,
QWeatherTypeOptions,
QWeatherUserTypeOptions,
} from '../../options';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
enum FormFieldName {
Type = 'type',
UserType = 'user_type',
}
const QWeatherForm = ({ form, node }: INextOperatorForm) => {
const { t } = useTranslation();
const typeValue = form.watch(FormFieldName.Type);
const qWeatherLangOptions = useMemo(() => {
return QWeatherLangOptions.map((x) => ({
value: x,
label: t(`flow.qWeatherLangOptions.${x}`),
}));
}, [t]);
const qWeatherTypeOptions = useMemo(() => {
return QWeatherTypeOptions.map((x) => ({
value: x,
label: t(`flow.qWeatherTypeOptions.${x}`),
}));
}, [t]);
const qWeatherUserTypeOptions = useMemo(() => {
return QWeatherUserTypeOptions.map((x) => ({
value: x,
label: t(`flow.qWeatherUserTypeOptions.${x}`),
}));
}, [t]);
const getQWeatherTimePeriodOptions = useCallback(() => {
let options = QWeatherTimePeriodOptions;
const userType = form.getValues(FormFieldName.UserType);
if (userType === 'free') {
options = options.slice(0, 3);
}
return options.map((x) => ({
value: x,
label: t(`flow.qWeatherTimePeriodOptions.${x}`),
}));
}, [form, t]);
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<FormField
control={form.control}
name="web_apikey"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.webApiKey')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lang"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.lang')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={qWeatherLangOptions}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={FormFieldName.Type}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.type')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={qWeatherTypeOptions}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={FormFieldName.UserType}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.userType')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={qWeatherUserTypeOptions}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{typeValue === 'weather' && (
<FormField
control={form.control}
name={'time_period'}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.timePeriod')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={getQWeatherTimePeriodOptions()}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
</Form>
);
};
export default QWeatherForm;

View File

@ -1,5 +1,4 @@
import { Operator } from '../../constant';
import AkShareForm from '../akshare-form';
import ArXivForm from './arxiv-form';
import BingForm from './bing-form';
import CrawlerForm from './crawler-form';
@ -29,7 +28,6 @@ export const ToolFormConfigMap = {
[Operator.GoogleScholar]: GoogleScholarForm,
[Operator.GitHub]: GithubForm,
[Operator.ExeSQL]: ExeSQLForm,
[Operator.AkShare]: AkShareForm,
[Operator.YahooFinance]: YahooFinanceForm,
[Operator.Crawler]: CrawlerForm,
[Operator.Email]: EmailForm,

View File

@ -1,83 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { DatePicker, DatePickerProps, Form, Input, Select } from 'antd';
import dayjs from 'dayjs';
import { useCallback, useMemo } from 'react';
import { IOperatorForm } from '../../interface';
import { TuShareSrcOptions } from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable';
const DateTimePicker = ({
onChange,
value,
}: {
onChange?: (val: number | undefined) => void;
value?: number | undefined;
}) => {
const handleChange: DatePickerProps['onChange'] = useCallback(
(val: any) => {
const nextVal = val?.format('YYYY-MM-DD HH:mm:ss');
onChange?.(nextVal ? nextVal : undefined);
},
[onChange],
);
// The value needs to be converted into a string and saved to the backend
const nextValue = useMemo(() => {
if (value) {
return dayjs(value);
}
return undefined;
}, [value]);
return (
<DatePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
onChange={handleChange}
value={nextValue}
/>
);
};
const TuShareForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const tuShareSrcOptions = useMemo(() => {
return TuShareSrcOptions.map((x) => ({
value: x,
label: t(`tuShareSrcOptions.${x}`),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
label={t('token')}
name={'token'}
tooltip={'Get from https://tushare.pro/'}
>
<Input></Input>
</Form.Item>
<Form.Item label={t('src')} name={'src'}>
<Select options={tuShareSrcOptions}></Select>
</Form.Item>
<Form.Item label={t('startDate')} name={'start_date'}>
<DateTimePicker />
</Form.Item>
<Form.Item label={t('endDate')} name={'end_date'}>
<DateTimePicker />
</Form.Item>
<Form.Item label={t('keyword')} name={'keyword'}>
<Input></Input>
</Form.Item>
</Form>
);
};
export default TuShareForm;

View File

@ -1,7 +1,7 @@
import { BoolSegmented } from '@/components/bool-segmented';
import JsonEditor from '@/components/json-edit';
import { BlockButton, Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Segmented } from '@/components/ui/segmented';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import { Trash2, X } from 'lucide-react';
@ -15,7 +15,7 @@ export const useObjectFields = () => {
(field: FieldValues, className?: string) => {
const fieldValue = field.value ? true : false;
return (
<Segmented
<BoolSegmented
options={
[
{ value: true, label: 'True' },
@ -27,7 +27,7 @@ export const useObjectFields = () => {
onChange={field.onChange}
className={className}
itemClassName="justify-center flex-1"
></Segmented>
></BoolSegmented>
);
},
[],

View File

@ -10,7 +10,6 @@ import {
NodeMap,
Operator,
initialAgentValues,
initialAkShareValues,
initialArXivValues,
initialBeginValues,
initialBingValues,
@ -29,14 +28,12 @@ import {
initialInvokeValues,
initialIterationStartValues,
initialIterationValues,
initialJin10Values,
initialKeywordExtractValues,
initialListOperationsValues,
initialLoopValues,
initialMessageValues,
initialNoteValues,
initialParserValues,
initialPubMedValues,
initialQWeatherValues,
initialRelevantValues,
initialRetrievalValues,
initialRewriteQuestionValues,
@ -47,7 +44,6 @@ import {
initialTavilyExtractValues,
initialTavilyValues,
initialTokenizerValues,
initialTuShareValues,
initialUserFillUpValues,
initialVariableAggregatorValues,
initialVariableAssignerValues,
@ -68,6 +64,63 @@ function isBottomSubAgent(type: string, position: Position) {
type === Operator.Tool
);
}
const GroupStartNodeMap = {
[Operator.Iteration]: {
id: `${Operator.IterationStart}:${humanId()}`,
type: 'iterationStartNode',
position: { x: 50, y: 100 },
data: {
label: Operator.IterationStart,
name: Operator.IterationStart,
form: initialIterationStartValues,
},
extent: 'parent' as 'parent',
},
[Operator.Loop]: {
id: `${Operator.LoopStart}:${humanId()}`,
type: 'loopStartNode',
position: { x: 50, y: 100 },
data: {
label: Operator.LoopStart,
name: Operator.LoopStart,
form: {},
},
extent: 'parent' as 'parent',
},
};
function useAddGroupNode() {
const { addEdge, addNode } = useGraphStore((state) => state);
const addGroupNode = useCallback(
(operatorType: string, newNode: Node<any>, nodeId?: string) => {
newNode.width = 500;
newNode.height = 250;
const startNode: Node<any> =
GroupStartNodeMap[operatorType as keyof typeof GroupStartNodeMap];
startNode.parentId = newNode.id;
addNode(newNode);
addNode(startNode);
if (nodeId) {
addEdge({
source: nodeId,
target: newNode.id,
sourceHandle: NodeHandleId.Start,
targetHandle: NodeHandleId.End,
});
}
return newNode.id;
},
[addEdge, addNode],
);
return { addGroupNode };
}
export const useInitializeOperatorParams = () => {
const llmId = useFetchModelId();
@ -82,10 +135,6 @@ export const useInitializeOperatorParams = () => {
llm_id: llmId,
},
[Operator.Message]: initialMessageValues,
[Operator.KeywordExtract]: {
...initialKeywordExtractValues,
llm_id: llmId,
},
[Operator.DuckDuckGo]: initialDuckValues,
[Operator.Wikipedia]: initialWikipediaValues,
[Operator.PubMed]: initialPubMedValues,
@ -95,14 +144,10 @@ export const useInitializeOperatorParams = () => {
[Operator.GoogleScholar]: initialGoogleScholarValues,
[Operator.SearXNG]: initialSearXNGValues,
[Operator.GitHub]: initialGithubValues,
[Operator.QWeather]: initialQWeatherValues,
[Operator.ExeSQL]: initialExeSqlValues,
[Operator.Switch]: initialSwitchValues,
[Operator.WenCai]: initialWenCaiValues,
[Operator.AkShare]: initialAkShareValues,
[Operator.YahooFinance]: initialYahooFinanceValues,
[Operator.Jin10]: initialJin10Values,
[Operator.TuShare]: initialTuShareValues,
[Operator.Note]: initialNoteValues,
[Operator.Crawler]: initialCrawlerValues,
[Operator.Invoke]: initialInvokeValues,
@ -133,6 +178,9 @@ export const useInitializeOperatorParams = () => {
[Operator.ListOperations]: initialListOperationsValues,
[Operator.VariableAssigner]: initialVariableAssignerValues,
[Operator.VariableAggregator]: initialVariableAggregatorValues,
[Operator.Loop]: initialLoopValues,
[Operator.LoopStart]: {},
[Operator.ExitLoop]: {},
};
}, [llmId]);
@ -311,6 +359,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
const { addChildEdge } = useAddChildEdge();
const { addToolNode } = useAddToolNode();
const { resizeIterationNode } = useResizeIterationNode();
const { addGroupNode } = useAddGroupNode();
// const [reactFlowInstance, setReactFlowInstance] =
// useState<ReactFlowInstance<any, any>>();
@ -376,33 +425,8 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
}
}
if (type === Operator.Iteration) {
newNode.width = 500;
newNode.height = 250;
const iterationStartNode: Node<any> = {
id: `${Operator.IterationStart}:${humanId()}`,
type: 'iterationStartNode',
position: { x: 50, y: 100 },
// draggable: false,
data: {
label: Operator.IterationStart,
name: Operator.IterationStart,
form: initialIterationStartValues,
},
parentId: newNode.id,
extent: 'parent',
};
addNode(newNode);
addNode(iterationStartNode);
if (nodeId) {
addEdge({
source: nodeId,
target: newNode.id,
sourceHandle: NodeHandleId.Start,
targetHandle: NodeHandleId.End,
});
}
return newNode.id;
if ([Operator.Iteration, Operator.Loop].includes(type as Operator)) {
return addGroupNode(type, newNode, nodeId);
} else if (
type === Operator.Agent &&
params.position === Position.Bottom
@ -456,6 +480,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
[
addChildEdge,
addEdge,
addGroupNode,
addNode,
addToolNode,
calculateNewlyBackChildPosition,

View File

@ -1,4 +1,4 @@
import { buildNodeOutputOptions } from '@/utils/canvas-util';
import { buildUpstreamNodeOutputOptions } from '@/utils/canvas-util';
import { useMemo } from 'react';
import { Operator } from '../constant';
import OperatorIcon from '../operator-icon';
@ -9,7 +9,7 @@ export function useBuildNodeOutputOptions(nodeId?: string) {
const edges = useGraphStore((state) => state.edges);
return useMemo(() => {
return buildNodeOutputOptions({
return buildUpstreamNodeOutputOptions({
nodes,
edges,
nodeId,

View File

@ -0,0 +1,10 @@
import { filterChildNodeIds } from '@/utils/canvas-util';
import useGraphStore from '../store';
export function useFilterChildNodeIds(nodeId?: string) {
const nodes = useGraphStore((state) => state.nodes);
const childNodeIds = filterChildNodeIds(nodes, nodeId);
return childNodeIds ?? [];
}

View File

@ -1,15 +1,21 @@
import { AgentGlobals, AgentStructuredOutputField } from '@/constants/agent';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { buildNodeOutputOptions, isAgentStructured } from '@/utils/canvas-util';
import {
buildNodeOutputOptions,
buildOutputOptions,
buildUpstreamNodeOutputOptions,
isAgentStructured,
} from '@/utils/canvas-util';
import { DefaultOptionType } from 'antd/es/select';
import { t } from 'i18next';
import { isEmpty, toLower } from 'lodash';
import { flatten, isEmpty, toLower } from 'lodash';
import get from 'lodash/get';
import { MessageSquareCode } from 'lucide-react';
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import {
AgentDialogueMode,
AgentVariableType,
BeginId,
BeginQueryType,
JsonSchemaDataType,
@ -85,20 +91,39 @@ export const useGetBeginNodeDataQueryIsSafe = () => {
return isBeginNodeDataQuerySafe;
};
export function useBuildNodeOutputOptions(nodeId?: string) {
export function useBuildUpstreamNodeOutputOptions(nodeId?: string) {
const nodes = useGraphStore((state) => state.nodes);
const edges = useGraphStore((state) => state.edges);
return useMemo(() => {
return buildNodeOutputOptions({
return buildUpstreamNodeOutputOptions({
nodes,
edges,
nodeId,
Icon: ({ name }) => <OperatorIcon name={name as Operator}></OperatorIcon>,
});
}, [edges, nodeId, nodes]);
}
export function useBuildParentOutputOptions(parentId?: string) {
const { getNode, getOperatorTypeFromId } = useGraphStore((state) => state);
const parentNode = getNode(parentId);
const parentType = getOperatorTypeFromId(parentId);
if (
parentType &&
[Operator.Loop].includes(parentType as Operator) &&
parentNode
) {
const options = buildOutputOptions(parentNode);
if (options) {
return [options];
}
}
return [];
}
// exclude nodes with branches
const ExcludedNodes = [
Operator.Categorize,
@ -120,7 +145,7 @@ function transferToVariableType(type: string) {
return type;
}
export function useBuildBeginVariableOptions() {
export function useBuildBeginDynamicVariableOptions() {
const inputs = useSelectBeginNodeDataInputs();
const options = useMemo(() => {
@ -144,6 +169,30 @@ export function useBuildBeginVariableOptions() {
const Env = 'env.';
export function useBuildGlobalWithBeginVariableOptions() {
const { data } = useFetchAgent();
const dynamicBeginOptions = useBuildBeginDynamicVariableOptions();
const globals = data?.dsl?.globals ?? {};
const globalOptions = Object.entries(globals)
.filter(([key]) => !key.startsWith(Env))
.map(([key, value]) => ({
label: key,
value: key,
icon: <OperatorIcon name={Operator.Begin} className="block" />,
parentLabel: <span>{t('flow.beginInput')}</span>,
type: Array.isArray(value)
? `${VariableType.Array}${key === AgentGlobals.SysFiles ? '<file>' : ''}`
: typeof value,
}));
return [
{
...dynamicBeginOptions[0],
options: [...(dynamicBeginOptions[0]?.options ?? []), ...globalOptions],
},
];
}
export function useBuildConversationVariableOptions() {
const { data } = useFetchAgent();
@ -175,55 +224,88 @@ export function useBuildConversationVariableOptions() {
}
export const useBuildVariableOptions = (nodeId?: string, parentId?: string) => {
const nodeOutputOptions = useBuildNodeOutputOptions(nodeId);
const parentNodeOutputOptions = useBuildNodeOutputOptions(parentId);
const beginOptions = useBuildBeginVariableOptions();
const upstreamNodeOutputOptions = useBuildUpstreamNodeOutputOptions(nodeId);
const parentNodeOutputOptions = useBuildParentOutputOptions(parentId);
const parentUpstreamNodeOutputOptions =
useBuildUpstreamNodeOutputOptions(parentId);
const options = useMemo(() => {
return [...beginOptions, ...nodeOutputOptions, ...parentNodeOutputOptions];
}, [beginOptions, nodeOutputOptions, parentNodeOutputOptions]);
return [
...upstreamNodeOutputOptions,
...parentNodeOutputOptions,
...parentUpstreamNodeOutputOptions,
];
}, [
upstreamNodeOutputOptions,
parentNodeOutputOptions,
parentUpstreamNodeOutputOptions,
]);
return options;
};
export function useBuildQueryVariableOptions(n?: RAGFlowNodeType) {
const { data } = useFetchAgent();
export type BuildQueryVariableOptions = {
nodeIds?: string[];
variablesExceptOperatorOutputs?: AgentVariableType[];
};
export function useBuildQueryVariableOptions({
n,
nodeIds = [],
variablesExceptOperatorOutputs, // Variables other than operator output variables
}: {
n?: RAGFlowNodeType;
} & BuildQueryVariableOptions = {}) {
const node = useContext(AgentFormContext) || n;
const nodes = useGraphStore((state) => state.nodes);
const options = useBuildVariableOptions(node?.id, node?.parentId);
const conversationOptions = useBuildConversationVariableOptions();
const globalWithBeginVariableOptions =
useBuildGlobalWithBeginVariableOptions();
const AgentVariableOptionsMap = {
[AgentVariableType.Begin]: globalWithBeginVariableOptions,
[AgentVariableType.Conversation]: conversationOptions,
};
const nextOptions = useMemo(() => {
const globals = data?.dsl?.globals ?? {};
const globalOptions = Object.entries(globals)
.filter(([key]) => !key.startsWith(Env))
.map(([key, value]) => ({
label: key,
value: key,
icon: <OperatorIcon name={Operator.Begin} className="block" />,
parentLabel: <span>{t('flow.beginInput')}</span>,
type: Array.isArray(value)
? `${VariableType.Array}${key === AgentGlobals.SysFiles ? '<file>' : ''}`
: typeof value,
}));
return [
...globalWithBeginVariableOptions,
...conversationOptions,
...options,
];
}, [conversationOptions, globalWithBeginVariableOptions, options]);
// Which options are entirely under external control?
if (!isEmpty(nodeIds) || !isEmpty(variablesExceptOperatorOutputs)) {
const nodeOutputOptions = buildNodeOutputOptions({ nodes, nodeIds });
const variablesExceptOperatorOutputsOptions =
variablesExceptOperatorOutputs?.map((x) => AgentVariableOptionsMap[x]) ??
[];
return [
{
...options[0],
options: [...options[0]?.options, ...globalOptions],
},
...options.slice(1),
...conversationOptions,
...flatten(variablesExceptOperatorOutputsOptions),
...nodeOutputOptions,
];
}, [conversationOptions, data?.dsl?.globals, options]);
}
return nextOptions;
}
export function useFilterQueryVariableOptionsByTypes(
types?: JsonSchemaDataType[],
) {
const nextOptions = useBuildQueryVariableOptions();
export function useFilterQueryVariableOptionsByTypes({
types,
nodeIds = [],
variablesExceptOperatorOutputs,
}: {
types?: JsonSchemaDataType[];
} & BuildQueryVariableOptions) {
const nextOptions = useBuildQueryVariableOptions({
nodeIds,
variablesExceptOperatorOutputs,
});
const filteredOptions = useMemo(() => {
return !isEmpty(types)
@ -294,7 +376,7 @@ export function useBuildComponentIdAndBeginOptions(
parentId?: string,
) {
const componentIdOptions = useBuildComponentIdOptions(nodeId, parentId);
const beginOptions = useBuildBeginVariableOptions();
const beginOptions = useBuildBeginDynamicVariableOptions();
return [...beginOptions, ...componentIdOptions];
}
@ -323,9 +405,19 @@ export function flatOptions(options: DefaultOptionType[]) {
}, []);
}
export function useFlattenQueryVariableOptions(nodeId?: string) {
export function useFlattenQueryVariableOptions({
nodeId,
nodeIds = [],
variablesExceptOperatorOutputs,
}: {
nodeId?: string;
} & BuildQueryVariableOptions = {}) {
const { getNode } = useGraphStore((state) => state);
const nextOptions = useBuildQueryVariableOptions(getNode(nodeId));
const nextOptions = useBuildQueryVariableOptions({
n: getNode(nodeId),
nodeIds,
variablesExceptOperatorOutputs,
});
const flattenOptions = useMemo(() => {
return flatOptions(nextOptions);
@ -334,8 +426,18 @@ export function useFlattenQueryVariableOptions(nodeId?: string) {
return flattenOptions;
}
export function useGetVariableLabelOrTypeByValue(nodeId?: string) {
const flattenOptions = useFlattenQueryVariableOptions(nodeId);
export function useGetVariableLabelOrTypeByValue({
nodeId,
nodeIds = [],
variablesExceptOperatorOutputs,
}: {
nodeId?: string;
} & BuildQueryVariableOptions = {}) {
const flattenOptions = useFlattenQueryVariableOptions({
nodeId,
nodeIds,
variablesExceptOperatorOutputs,
});
const findAgentStructuredOutputTypeByValue =
useFindAgentStructuredOutputTypeByValue();
const findAgentStructuredOutputLabel =

View File

@ -14,6 +14,7 @@ export const useShowFormDrawer = () => {
setClickedNodeId,
getNode,
setClickedToolId,
getOperatorTypeFromId,
} = useGraphStore((state) => state);
const {
visible: formDrawerVisible,
@ -25,14 +26,20 @@ export const useShowFormDrawer = () => {
(e: React.MouseEvent<Element>, nodeId: string) => {
const tool = get(e.target, 'dataset.tool');
// TODO: Operator type judgment should be used
if (nodeId.startsWith(Operator.Tool) && !tool) {
const operatorType = getOperatorTypeFromId(nodeId);
if (
(operatorType === Operator.Tool && !tool) ||
[Operator.LoopStart, Operator.ExitLoop].includes(
operatorType as Operator,
)
) {
return;
}
setClickedNodeId(nodeId);
setClickedToolId(tool);
showFormDrawer();
},
[setClickedNodeId, setClickedToolId, showFormDrawer],
[getOperatorTypeFromId, setClickedNodeId, setClickedToolId, showFormDrawer],
);
return {

View File

@ -41,3 +41,11 @@ export type IInputs = {
prologue: string;
mode: string;
};
export type IOutputs = Record<
string,
{
type?: string;
value?: string;
}
>;

View File

@ -14,7 +14,12 @@ import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.s
import { IconFontFill } from '@/components/icon-font';
import { cn } from '@/lib/utils';
import { FileCode, HousePlus } from 'lucide-react';
import {
FileCode,
HousePlus,
Infinity as InfinityIcon,
LogOut,
} from 'lucide-react';
import { Operator } from './constant';
interface IProps {
@ -60,6 +65,8 @@ export const SVGIconMap = {
};
export const LucideIconMap = {
[Operator.DataOperations]: FileCode,
[Operator.Loop]: InfinityIcon,
[Operator.ExitLoop]: LogOut,
};
const Empty = () => {

View File

@ -8,7 +8,7 @@ import {
} from '@/interfaces/database/agent';
import { DSLComponents, RAGFlowNodeType } from '@/interfaces/database/flow';
import { buildSelectOptions } from '@/utils/component-util';
import { removeUselessFieldsFromValues } from '@/utils/form';
import { buildOptions, removeUselessFieldsFromValues } from '@/utils/form';
import { Edge, Node, XYPosition } from '@xyflow/react';
import { FormInstance, FormListFieldData } from 'antd';
import { humanId } from 'human-id';
@ -27,6 +27,7 @@ import {
CategorizeAnchorPointPositions,
FileType,
FileTypeSuffixMap,
InputMode,
NoCopyOperatorsList,
NoDebugOperatorsList,
NodeHandleId,
@ -772,3 +773,5 @@ export function getArrayElementType(type: string) {
export function buildConversationVariableSelectOptions() {
return buildSelectOptions(Object.values(TypesWithArray));
}
export const InputModeOptions = buildOptions(InputMode);

View File

@ -4,10 +4,11 @@ import {
Operator,
} from '@/constants/agent';
import { BaseNode } from '@/interfaces/database/agent';
import OperatorIcon from '@/pages/agent/operator-icon';
import { Edge } from '@xyflow/react';
import { get, isEmpty } from 'lodash';
import { ComponentType, ReactNode } from 'react';
import { ReactNode } from 'react';
export function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) {
return nodeIds.reduce<string[]>((pre, nodeId) => {
@ -29,13 +30,21 @@ export function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) {
}, []);
}
export function filterChildNodeIds(nodes: BaseNode[], nodeId?: string) {
return nodes.filter((x) => x.parentId === nodeId).map((x) => x.id);
}
export function isAgentStructured(id?: string, label?: string) {
return (
label === AgentStructuredOutputField && id?.startsWith(`${Operator.Agent}:`)
);
}
export function buildOutputOptions(
export function buildVariableValue(value: string, nodeId?: string) {
return `${nodeId}@${value}`;
}
export function buildSecondaryOutputOptions(
outputs: Record<string, any> = {},
nodeId?: string,
parentLabel?: string | ReactNode,
@ -43,7 +52,7 @@ export function buildOutputOptions(
) {
return Object.keys(outputs).map((x) => ({
label: x,
value: `${nodeId}@${x}`,
value: buildVariableValue(x, nodeId),
parentLabel,
icon,
type: isAgentStructured(nodeId, x)
@ -52,40 +61,63 @@ export function buildOutputOptions(
}));
}
export function buildOutputOptions(x: BaseNode) {
return {
label: x.data.name,
value: x.id,
title: x.data.name,
options: buildSecondaryOutputOptions(
x.data.form.outputs,
x.id,
x.data.name,
<OperatorIcon name={x.data.label as Operator} />,
),
};
}
export function buildNodeOutputOptions({
nodes, // all nodes
nodeIds, // Need to obtain the output node IDs
}: {
nodes: BaseNode[];
nodeIds: string[];
}) {
const nodeWithOutputList = nodes.filter(
(x) => nodeIds.some((y) => y === x.id) && !isEmpty(x.data?.form?.outputs),
);
return nodeWithOutputList.map((x) => buildOutputOptions(x));
}
export function buildUpstreamNodeOutputOptions({
nodes,
edges,
nodeId,
Icon,
}: {
nodes: BaseNode[];
edges: Edge[];
nodeId?: string;
Icon: ComponentType<{ name: string }>;
}) {
if (!nodeId) {
return [];
}
const upstreamIds = filterAllUpstreamNodeIds(edges, [nodeId]);
return buildNodeOutputOptions({ nodes, nodeIds: upstreamIds });
}
export function buildChildOutputOptions({
nodes,
nodeId,
}: {
nodes: BaseNode[];
nodeId?: string;
}) {
const nodeWithOutputList = nodes.filter(
(x) =>
upstreamIds.some((y) => y === x.id) && !isEmpty(x.data?.form?.outputs),
(x) => x.parentId === nodeId && !isEmpty(x.data?.form?.outputs),
);
return nodeWithOutputList
.filter((x) => x.id !== nodeId)
.map((x) => ({
label: x.data.name,
value: x.id,
title: x.data.name,
options: buildOutputOptions(
x.data.form.outputs,
x.id,
x.data.name,
<Icon name={x.data.name} />,
),
}));
return nodeWithOutputList.map((x) => buildOutputOptions(x));
}
export function getStructuredDatatype(value: Record<string, any> | unknown) {