feat(web): model select component replace

This commit is contained in:
zhaoying
2026-03-07 17:18:27 +08:00
parent 153e68e055
commit 509d1a2e24
10 changed files with 501 additions and 441 deletions

View File

@@ -0,0 +1,87 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-07 16:49:59
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-07 17:14:57
*/
import { useEffect, useState, type FC } from 'react';
import { Select, Flex, Space } from 'antd';
import type { SelectProps } from 'antd/es/select';
import { useTranslation } from 'react-i18next';
import { getModelList } from '@/api/models';
import type { Query, Model } from '@/views/ModelManagement/types';
import { getListLogoUrl } from '@/views/ModelManagement/utils';
import Tag from '@/components/Tag';
/** Extends AntD SelectProps; omits filterOption since it's handled internally */
interface ModelSelectProps extends SelectProps {
/** Extra query params passed to getModelList */
params?: Query;
placeholder?: string;
}
const ModelSelect: FC<ModelSelectProps> = ({
params,
placeholder,
...props
}) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Model[]>([]);
// Fetch active models whenever params change; stringify for stable deep comparison
useEffect(() => {
getModelList({
...(params ?? {}),
pagesize: 100,
is_active: true
}).then((res) => {
setOptions((res as { items: Model[] }).items ?? []);
});
}, [JSON.stringify(params)]);
// Render the selected value inside the trigger with logo + truncated name
const labelRender: SelectProps['labelRender'] = ({ value }) => {
const item = options.find((o) => o.id === value);
if (!item) return undefined;
const logo = getListLogoUrl(item.provider, item.logo as string);
return (
<Flex align="center" gap={8}>
{logo && <img src={logo} className="rb:size-5 rb:rounded-md" alt="" />}
<div className="rb:flex-1 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{item.name}</div>
</Flex>
);
};
return (
<Select
placeholder={placeholder ?? t('common.pleaseSelect')}
options={options}
fieldNames={{ label: 'name', value: 'id' }}
allowClear
popupMatchSelectWidth={false}
labelRender={labelRender}
// Each dropdown option shows logo, name, and capability tags
optionRender={(option) => {
const { data } = option;
const logo = getListLogoUrl(data.provider, data.logo as string);
return (
<Flex align="center" gap={8}>
<Flex align="center" gap={8}>
{logo && <img src={logo} className="rb:size-5 rb:rounded-md" alt="" />}
<span className="rb:wrap-break-word rb:line-clamp-1">{data.name as string}</span>
</Flex>
{data.capability?.length > 0 && (
<Space size={4}>
{data.capability.map((vo: string) => <Tag key={vo}>{vo}</Tag>)}
</Space>
)}
</Flex>
);
}}
{...props}
/>
);
};
export default ModelSelect;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 10:20:16
* @Last Modified time: 2026-03-07 17:11:54
*/
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next'
@@ -11,7 +11,6 @@ import { Form, Space, Row, Col, Button, Flex, App, Select, Spin } from 'antd'
import Card from './components/Card'
import Tag from './components/Tag'
import CustomSelect from '@/components/CustomSelect';
import { getMultiAgentConfig, saveMultiAgentConfig, getApplicationList } from '@/api/application';
import type {
Config,
@@ -26,7 +25,7 @@ import RbCard from '@/components/RbCard/Card'
import SubAgentModal from './components/SubAgentModal'
import Empty from '@/components/Empty'
import RadioGroupCard from '@/components/RadioGroupCard'
import { getModelListUrl } from '@/api/models'
import ModelSelect from '@/components/ModelSelect'
import ModelConfigModal from './components/ModelConfigModal'
import type { Application } from '@/views/ApplicationManagement/types'
@@ -268,13 +267,9 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
>
<Flex align="center" gap={12}>
<Form.Item name="default_model_config_id" noStyle>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{ width: '100%' }}
<ModelSelect
params={{ type: 'llm,chat' }}
className="rb:w-full!"
/>
</Form.Item>
<Form.Item name="model_parameters" noStyle>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:44
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 14:40:55
* @Last Modified time: 2026-03-07 17:12:25
*/
/**
* AI Prompt Assistant Modal
@@ -17,19 +17,17 @@ import clsx from 'clsx'
import copy from 'copy-to-clipboard';
import { updatePromptMessages, createPromptSessions } from '@/api/prompt'
import { getModelListUrl } from '@/api/models'
import type { AiPromptModalRef, AiPromptVariableModalRef, AiPromptForm } from '../types'
import RbModal from '@/components/RbModal'
import type { ModelListItem } from '@/views/ModelManagement/types'
import ChatContent from '@/components/Chat/ChatContent'
import Empty from '@/components/Empty'
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
import type { ChatItem } from '@/components/Chat/types'
import CustomSelect from '@/components/CustomSelect'
import type { ChatItem } from '@/components/Chat/types'
import ModelSelect from '@/components/ModelSelect'
import AiPromptVariableModal from './AiPromptVariableModal'
import { type SSEMessage } from '@/utils/stream'
import Editor from './Editor'
import { getLogoUrl } from '@/views/ModelManagement/utils'
import analysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.png'
/**
@@ -215,23 +213,9 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
name="model_id"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
hasAll={false}
optionLabelProp="children"
format={(data) => {
return data.map(option => ({
...data,
value: option.id,
label: (<div key={option.id} className="rb:flex rb:items-center rb:gap-2">
{getLogoUrl(option.logo as string) && <img src={getLogoUrl(option.logo as string)} className="rb:inline-block rb:size-4 rb:align-middle" alt="" />}
<span>{option.name as string}</span>
</div>
)
}))
}}
className="rb:w-full"
<ModelSelect
params={{ type: 'llm,chat' }}
className="rb:w-full!"
/>
</Form.Item>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:42
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-24 11:47:32
* @Last Modified time: 2026-03-07 17:03:22
*/
/**
* Knowledge Global Configuration Modal
@@ -15,8 +15,7 @@ import { useTranslation } from 'react-i18next';
import type { RerankerConfig, KnowledgeGlobalConfigModalRef } from './types'
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import { getModelListUrl } from '@/api/models'
import ModelSelect from '@/components/ModelSelect'
const FormItem = Form.Item;
@@ -115,12 +114,9 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
rules={[{ required: true, message: t('common.pleaseSelect') }]}
extra={t('application.rearrangementModelDesc')}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
<ModelSelect
params={{ type: 'rerank' }}
className="rb:w-full!"
/>
</FormItem>
{/* Top K */}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:50:18
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-06 12:26:11
* @Last Modified time: 2026-03-07 16:14:25
*/
/**
* Type definitions for Model Management
@@ -115,6 +115,8 @@ export interface ModelApiKey {
updated_at: number;
/** Associated model config IDs */
model_config_ids: string[];
capability: Capability[];
is_omni?: boolean;
}
/**
@@ -324,4 +326,24 @@ export interface BaseRef {
/** Refresh list data */
getList: () => void;
modelListDetailRefresh?: () => void;
}
export type Capability = 'vision' | 'audio' | 'video';
export interface Model {
name: string;
type: string;
logo: string;
description: string | null;
provider: string;
config: Record<string, unknown>;
is_active: boolean;
is_public: boolean;
load_balance_strategy: string;
capability: Capability[];
is_omni: boolean;
model_id: string | null;
id: string;
created_at: number;
updated_at: number;
api_keys: ModelApiKey[];
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 17:44:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 16:35:47
* @Last Modified time: 2026-03-07 17:15:05
*/
/**
* Prompt Editor Component
@@ -17,18 +17,16 @@ import copy from 'copy-to-clipboard';
import { useNavigate, useLocation } from 'react-router-dom';
import { updatePromptMessages, createPromptSessions } from '@/api/prompt'
import { getModelListUrl } from '@/api/models'
import type { PromptVariableModalRef, AiPromptForm, HistoryItem, PromptSaveModalRef } from './types'
import ChatContent from '@/components/Chat/ChatContent'
import Empty from '@/components/Empty'
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
import type { ChatItem } from '@/components/Chat/types'
import CustomSelect from '@/components/CustomSelect'
import type { ChatItem } from '@/components/Chat/types'
import ModelSelect from '@/components/ModelSelect'
import PromptVariableModal from './components/PromptVariableModal'
import { type SSEMessage } from '@/utils/stream'
import Editor from '@/views/ApplicationConfig/components/Editor'
import PromptSaveModal from './components/PromptSaveModal'
import { getLogoUrl } from '@/views/ModelManagement/utils'
import analysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.png'
import Header from './components/Header';
import RbCard from '@/components/RbCard/Card';
@@ -235,22 +233,8 @@ const Prompt: FC = () => {
name="model_id"
noStyle
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
hasAll={false}
optionLabelProp="children"
format={(data) => {
return data.map(option => ({
...data,
value: option.id,
label: (<div key={option.id} className="rb:flex rb:items-center rb:gap-2">
{getLogoUrl(option.logo as string) && <img src={getLogoUrl(option.logo as string)} className="rb:inline-block rb:size-4 rb:align-middle" alt="" />}
<span>{option.name as string}</span>
</div>
)
}))
}}
<ModelSelect
params={{ type: 'llm,chat' }}
className={`rb:w-75! ${styles.select}`}
/>
</Form.Item>
@@ -275,6 +259,7 @@ const Prompt: FC = () => {
>{t('common.copy')}</Button>
<Button
icon={<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/plus_dark.svg')]"></div>}
disabled={!values?.current_prompt || loading}
onClick={handleAdd}
></Button>
</Space>}
@@ -286,7 +271,7 @@ const Prompt: FC = () => {
className="rb:h-[calc(100vh-201px)] rb:bg-white! rb:border-none! rb:p-0! rb:text-[#212332] rb:leading-5"
onChange={(value) => form.setFieldValue('current_prompt', value)}
/>
: <Empty url={analysisEmptyIcon} title={t(`prompt.promptPlaceholder`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-119 rb:w-70 rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
: <Empty url={analysisEmptyIcon} title={t(`prompt.promptPlaceholder`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-[calc(100vh-201px)] rb:w-70 rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
}
</Form.Item>
</RbCard>

View File

@@ -4,8 +4,7 @@ import { useTranslation } from 'react-i18next';
import type { RerankerConfig, KnowledgeGlobalConfigModalRef } from './types'
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import { getModelListUrl } from '@/api/models'
import ModelSelect from '@/components/ModelSelect'
const FormItem = Form.Item;
@@ -96,12 +95,9 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
rules={[{ required: true, message: t('common.pleaseSelect') }]}
extra={t('application.rearrangementModelDesc')}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
<ModelSelect
params={{ type: 'rerank' }}
className="rb:w-full!"
/>
</FormItem>
{/* Top K */}

View File

@@ -4,8 +4,7 @@ import { Form } from 'antd'
import RbSlider from '@/components/RbSlider'
import RbCard from '@/components/RbCard/Card'
import CustomSelect from '@/components/CustomSelect'
import { getModelListUrl } from '@/api/models'
import ModelSelect from '@/components/ModelSelect'
const ModelConfig: FC = () => {
const { t } = useTranslation()
@@ -20,13 +19,10 @@ const ModelConfig: FC = () => {
label={t('workflow.config.llm.model_id')}
className={model_id ? 'rb:mb-2!' : 'rb:mb-4!'}
>
<CustomSelect
<ModelSelect
placeholder={t('common.pleaseSelect')}
url={getModelListUrl}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
hasAll={false}
valueKey="id"
labelKey="name"
params={{ type: 'llm,chat' }}
className="rb:w-full!"
size="small"
/>
</Form.Item>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:39:59
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-05 17:48:25
* @Last Modified time: 2026-03-07 17:16:13
*/
import { type FC, useEffect, useState, useMemo } from "react";
import clsx from 'clsx'
@@ -36,6 +36,7 @@ import CodeExecution from './CodeExecution'
import { nodeLibrary } from '../../constant';
import RbCard from '@/components/RbCard/Card';
import ModelConfig from './ModelConfig'
import ModelSelect from '@/components/ModelSelect'
/**
* Props for Properties component
@@ -72,7 +73,7 @@ const Properties: FC<PropertiesProps> = ({
}) => {
const { t } = useTranslation()
const [form] = Form.useForm<NodeConfig>();
const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>)
const [configs, setConfigs] = useState<Record<string, NodeConfig>>({} as Record<string, NodeConfig>)
const values = Form.useWatch([], form);
const variableList = useVariableList(selectedNode, graphRef, chatVariables)
@@ -169,7 +170,7 @@ const Properties: FC<PropertiesProps> = ({
const nodes = graphRef.current?.getNodes() || [];
const nodeData = selectedNode.getData();
const cycle = nodeData?.cycle;
if (cycle) {
const parentNode = nodes.find(n => n.getData().id === cycle);
if (parentNode) {
@@ -187,14 +188,14 @@ const Properties: FC<PropertiesProps> = ({
if (parentIterationNode) {
const parentData = parentIterationNode.getData();
const parentNodeId = parentData.id;
if (parentData.config?.input?.defaultValue) {
const itemKey = `${parentNodeId}_item`;
const indexKey = `${parentNodeId}_index`;
const existingItemVar = filteredList.find(v => v.key === itemKey);
const existingIndexVar = filteredList.find(v => v.key === indexKey);
if (!existingItemVar) {
// Determine item dataType from input variable
let itemDataType = 'object';
@@ -202,7 +203,7 @@ const Properties: FC<PropertiesProps> = ({
if (inputVariable && inputVariable.dataType.startsWith('array[')) {
itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1');
}
filteredList.push({
key: itemKey,
label: 'item',
@@ -212,7 +213,7 @@ const Properties: FC<PropertiesProps> = ({
nodeData: parentData,
});
}
if (!existingIndexVar) {
filteredList.push({
key: indexKey,
@@ -234,7 +235,7 @@ const Properties: FC<PropertiesProps> = ({
const nodes = graphRef.current?.getNodes() || [];
const nodeData = selectedNode.getData();
const cycle = nodeData?.cycle;
if (cycle) {
const parentNode = nodes.find(n => n.getData().id === cycle);
if (parentNode) {
@@ -246,14 +247,14 @@ const Properties: FC<PropertiesProps> = ({
}
return null;
})() : null;
let filteredList = variableList.filter(variable => variable.dataType !== 'boolean');
// If this LLM node is a child of iteration/loop, ensure parent variables are included
if (parentLoopNode) {
const parentData = parentLoopNode.getData();
const parentNodeId = parentData.id;
// Ensure parent loop/iteration variables are included
if (parentData.type === 'loop') {
const cycleVars = parentData.cycle_vars || [];
@@ -276,10 +277,10 @@ const Properties: FC<PropertiesProps> = ({
if (parentData.config?.input?.defaultValue) {
const itemKey = `${parentNodeId}_item`;
const indexKey = `${parentNodeId}_index`;
const existingItemVar = filteredList.find(v => v.key === itemKey);
const existingIndexVar = filteredList.find(v => v.key === indexKey);
if (!existingItemVar) {
// Determine item dataType from input variable
let itemDataType = 'object';
@@ -287,7 +288,7 @@ const Properties: FC<PropertiesProps> = ({
if (inputVariable && inputVariable.dataType.startsWith('array[')) {
itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1');
}
filteredList.push({
key: itemKey,
label: 'item',
@@ -297,7 +298,7 @@ const Properties: FC<PropertiesProps> = ({
nodeData: parentData,
});
}
if (!existingIndexVar) {
filteredList.push({
key: indexKey,
@@ -311,7 +312,7 @@ const Properties: FC<PropertiesProps> = ({
}
}
}
return filteredList;
}
if (nodeType === 'knowledge-retrieval' || nodeType === 'parameter-extractor' && key !== 'prompt' || nodeType === 'memory-read' || nodeType === 'memory-write' || nodeType === 'question-classifier') {
@@ -324,10 +325,10 @@ const Properties: FC<PropertiesProps> = ({
}
if (nodeType === 'iteration' && key === 'output' || nodeType === 'loop' && key === 'condition') {
if (!selectedNode) return [];
let filteredList = nodeType === 'iteration'
? variableList.filter(variable => variable.value.includes('sys.'))
let filteredList = nodeType === 'iteration'
? variableList.filter(variable => variable.value.includes('sys.'))
: addParentIterationVars(variableList).filter(variable => variable.nodeData.type !== 'loop');
const childVariables = getChildNodeVariables(selectedNode, graphRef);
const existingKeys = new Set(filteredList.map(v => v.key));
childVariables.forEach(v => {
@@ -336,13 +337,13 @@ const Properties: FC<PropertiesProps> = ({
existingKeys.add(v.key);
}
});
return filteredList;
}
if (nodeType === 'iteration') {
return variableList.filter(variable => variable.dataType.includes('array'));
}
// For all other node types, add parent iteration variables if applicable
let baseList = variableList;
return addParentIterationVars(baseList);
@@ -416,7 +417,7 @@ const Properties: FC<PropertiesProps> = ({
}
}
const handleClick: MenuProps['onClick'] = (e) => {
switch(e.key) {
switch (e.key) {
case 'delete':
selectedNode.remove()
break;
@@ -433,7 +434,7 @@ const Properties: FC<PropertiesProps> = ({
<Dropdown
menu={{
items: [
{ key: 'delete', icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/common/delete_dark.svg')]"></div>, label: <Flex>{t('common.delete')}</Flex>},
{ key: 'delete', icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/common/delete_dark.svg')]"></div>, label: <Flex>{t('common.delete')}</Flex> },
// { key: 'copy', icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/copy_dark.svg')]"></div>, label: t('common.copy') }
],
onClick: handleClick
@@ -482,341 +483,348 @@ const Properties: FC<PropertiesProps> = ({
<Button type="primary" size="small" className="rb:text-[12px]!" onClick={handleSureReplace}>{t('workflow.sureReplace')}</Button>
</>
: selectedNode?.data?.type === 'http-request'
? <HttpRequest
options={variableList}
selectedNode={selectedNode}
graphRef={graphRef}
/>
: selectedNode?.data?.type === 'tool'
? <ToolConfig options={variableList} />
: selectedNode?.data?.type === 'jinja-render'
? <JinjaRender
selectedNode={selectedNode}
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')}
/>
: selectedNode?.data?.type === 'code'
? <CodeExecution
selectedNode={selectedNode}
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
/>
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
const config = configs[key] || {}
if (config.dependsOn && (values as any)?.[config.dependsOn as string] !== config.dependsOnValue) {
return null
}
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
return (
<Form.Item key={key} name={key} className="rb:mb-0!">
<VariableList
parentName={key}
? <HttpRequest
options={variableList}
selectedNode={selectedNode}
graphRef={graphRef}
/>
: selectedNode?.data?.type === 'tool'
? <ToolConfig options={variableList} />
: selectedNode?.data?.type === 'jinja-render'
? <JinjaRender
selectedNode={selectedNode}
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')}
/>
: selectedNode?.data?.type === 'code'
? <CodeExecution
selectedNode={selectedNode}
config={config}
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
/>
</Form.Item>
)
}
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
const config = configs[key] || {}
if (key === 'model_id' && selectedNode?.data?.type === 'llm') {
return <ModelConfig />
}
if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
// 为llm节点且isArray=true时添加context变量支持
let contextVariableList = [...getFilteredVariableList('llm')];
const isArrayMode = config.isArray !== false; // 默认为true
if (isArrayMode) {
const contextKey = `${selectedNode.id}_context`;
const hasContextVariable = contextVariableList.some(v => v.key === contextKey);
if (!hasContextVariable) {
contextVariableList.unshift({
key: contextKey,
label: 'context',
type: 'variable',
dataType: 'String',
value: `context`,
nodeData: selectedNode.getData(),
isContext: true,
});
}
}
return (
<Form.Item key={key} name={key}>
<MessageEditor
key={key}
options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
parentName={key}
placeholder={t(config.placeholder || 'common.pleaseSelect')}
size="small"
/>
</Form.Item>
)
}
if (selectedNode?.data?.type === 'iteration' && key === 'output_type') {
return (<Form.Item key={key} name={key} hidden />)
}
if (config.type === 'define') {
return null
}
if (config.type === 'knowledge') {
return (
<Form.Item
key={key}
name={key}
>
<Knowledge />
</Form.Item>
)
}
if (config.type === 'messageEditor') {
return (
<Form.Item key={key} name={key} label={selectedNode?.data?.type === 'memory-write' ? t(`workflow.config.${selectedNode?.data?.type}.${key}`) : undefined}>
<MessageEditor
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
placeholder={t(config.placeholder || 'common.pleaseEnter')}
isArray={!!config.isArray}
parentName={key}
language={config.language as LexicalEditorProps['language']}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
titleVariant={config.titleVariant}
size="small"
/>
</Form.Item>
)
}
if (config.type === 'paramList') {
return (
<Form.Item key={key} name={key}>
<ParamsList
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
/>
</Form.Item>
)
}
if (config.type === 'groupVariableList') {
return (
<Form.Item key={key} name={key}>
<GroupVariableList
name={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
isCanAdd={!!(values as any)?.group}
size="small"
/>
</Form.Item>
)
}
if (config.type === 'caseList') {
return (
<Form.Item key={key} name={key} noStyle>
<CaseList
name={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
selectedNode={selectedNode}
graphRef={graphRef}
/>
</Form.Item>
)
}
if (config.type === 'cycleVarsList') {
return (
<Form.Item key={key} name={key}>
<CycleVarsList
size="small"
parentName={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
/>
</Form.Item>
)
}
if (config.type === 'assignmentList') {
return (
<Form.Item key={key} name={key}>
<AssignmentList
parentName={key}
options={(() => {
if (config.filterLoopIterationVars) {
const loopIterationVars: Suggestion[] = [];
return [...getFilteredVariableList(selectedNode?.data?.type, key), ...loopIterationVars];
}
return getFilteredVariableList(selectedNode?.data?.type, key);
})()
if (config.dependsOn && (values as any)?.[config.dependsOn as string] !== config.dependsOnValue) {
return null
}
/>
</Form.Item>
)
}
if (config.type === 'memoryConfig') {
return (
<Form.Item
key={key}
name={key}
noStyle
>
<MemoryConfig
parentName={key}
options={getFilteredVariableList('llm')}
/>
</Form.Item>
)
}
if (config.type === 'conditionList') {
return (
<Form.Item
key={key}
name={key}
noStyle
>
<ConditionList
parentName={key}
options={(() => {
const cycleVars = values?.cycle_vars || [];
const cycleVarSuggestions: Suggestion[] = cycleVars.filter(vo => vo.name && vo.name.trim() !== '').map((cycleVar: any) => ({
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'String',
value: `${selectedNode.getData().id}.${cycleVar.name}`,
nodeData: selectedNode.getData(),
}));
return [...getFilteredVariableList(selectedNode?.data?.type, key), ...cycleVarSuggestions];
})()}
selectedNode={selectedNode}
graphRef={graphRef}
addBtnText={t('workflow.config.addCase')}
/>
</Form.Item>
)
}
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
return (
<Form.Item key={key} name={key} className="rb:mb-0!">
<VariableList
parentName={key}
selectedNode={selectedNode}
config={config}
/>
</Form.Item>
)
}
if (key === 'vision_input' && !values?.vision) {
return null
}
if (key === 'model_id' && selectedNode?.data?.type === 'llm') {
return <ModelConfig />
}
if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
// 为llm节点且isArray=true时添加context变量支持
let contextVariableList = [...getFilteredVariableList('llm')];
const isArrayMode = config.isArray !== false; // 默认为true
return (
<Form.Item
key={key}
name={key}
label={key === 'vision_input'
? undefined : key === 'parallel_count'
? <span className="rb:text-[10px] rb:text-[#5B6167] rb:leading-3.5 rb:-mb-1!">{t(`workflow.config.${selectedNode?.data?.type}.${key}`)}</span>
: t(`workflow.config.${selectedNode?.data?.type}.${key}`)
}
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
className={
key === 'parallel' && values?.parallel
? 'rb:mb-1!'
: key === 'vision' && values?.vision
? 'rb:mb-2!'
: key === 'group' && values?.group
? 'rb:mb-3!'
: ''
}
hidden={Boolean(config.hidden)}
>
{config.type === 'input'
? <Input placeholder={t('common.pleaseEnter')} />
: config.type === 'textarea'
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
: config.type === 'select'
? <Select
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
placeholder={t('common.pleaseSelect')}
/>
: config.type === 'inputNumber'
? <InputNumber
placeholder={t('common.pleaseEnter')}
className="rb:w-full!"
onChange={(value) => form.setFieldValue(key, value)}
/>
: config.type === 'slider'
? <RbSlider
min={config.min}
max={config.max}
step={config.step || 0.01}
isInput={true}
if (isArrayMode) {
const contextKey = `${selectedNode.id}_context`;
const hasContextVariable = contextVariableList.some(v => v.key === contextKey);
if (!hasContextVariable) {
contextVariableList.unshift({
key: contextKey,
label: 'context',
type: 'variable',
dataType: 'String',
value: `context`,
nodeData: selectedNode.getData(),
isContext: true,
});
}
}
return (
<Form.Item key={key} name={key}>
<MessageEditor
key={key}
options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
parentName={key}
placeholder={t(config.placeholder || 'common.pleaseSelect')}
size="small"
/>
: config.type === 'customSelect'
? <CustomSelect
placeholder={t('common.pleaseSelect')}
url={config.url as string}
params={config.params}
hasAll={false}
valueKey={config.valueKey}
labelKey={config.labelKey}
size="small"
/>
: config.type === 'variableList'
? <VariableSelect
placeholder={t(config.placeholder || 'common.pleaseSelect')}
options={(() => {
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
// Apply filtering if specified in config
if (config.filterNodeTypes || config.filterVariableNames) {
return baseVariableList.filter(variable => {
const nodeTypeMatch = !config.filterNodeTypes ||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
const variableNameMatch = !config.filterVariableNames ||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
return nodeTypeMatch || variableNameMatch;
});
}
if (config.onFilterVariableNames) {
return baseVariableList.filter(variable => Array.isArray(config.onFilterVariableNames) && config.onFilterVariableNames.includes(variable.label));
}
// Filter child nodes for iteration output
if (config.filterChildNodes && selectedNode) {
const graph = graphRef.current;
if (!graph) return [];
</Form.Item>
)
}
if (selectedNode?.data?.type === 'iteration' && key === 'output_type') {
return (<Form.Item key={key} name={key} hidden />)
}
if (config.type === 'define') {
return null
}
const nodes = graph.getNodes();
if (config.type === 'knowledge') {
return (
<Form.Item
key={key}
name={key}
>
<Knowledge />
</Form.Item>
)
}
// Find child nodes whose cycle field equals parent node's ID
const childNodes = nodes.filter(node => {
const nodeData = node.getData();
return nodeData?.cycle === selectedNode.id;
});
if (config.type === 'messageEditor') {
return (
<Form.Item key={key} name={key} label={selectedNode?.data?.type === 'memory-write' ? t(`workflow.config.${selectedNode?.data?.type}.${key}`) : undefined}>
<MessageEditor
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
placeholder={t(config.placeholder || 'common.pleaseEnter')}
isArray={!!config.isArray}
parentName={key}
language={config.language as LexicalEditorProps['language']}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
titleVariant={config.titleVariant}
size="small"
/>
</Form.Item>
)
}
return baseVariableList.filter(variable =>
childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
);
}
return baseVariableList;
})()}
onChange={(value, option) => handleChangeVariableList(value, option, key)}
size="small"
if (config.type === 'paramList') {
return (
<Form.Item key={key} name={key}>
<ParamsList
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
/>
</Form.Item>
)
}
if (config.type === 'groupVariableList') {
return (
<Form.Item key={key} name={key}>
<GroupVariableList
name={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
isCanAdd={!!(values as any)?.group}
size="small"
/>
</Form.Item>
)
}
if (config.type === 'caseList') {
return (
<Form.Item key={key} name={key} noStyle>
<CaseList
name={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
selectedNode={selectedNode}
graphRef={graphRef}
/>
</Form.Item>
)
}
if (config.type === 'cycleVarsList') {
return (
<Form.Item key={key} name={key}>
<CycleVarsList
size="small"
parentName={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
/>
</Form.Item>
)
}
if (config.type === 'assignmentList') {
return (
<Form.Item key={key} name={key}>
<AssignmentList
parentName={key}
options={(() => {
if (config.filterLoopIterationVars) {
const loopIterationVars: Suggestion[] = [];
return [...getFilteredVariableList(selectedNode?.data?.type, key), ...loopIterationVars];
}
return getFilteredVariableList(selectedNode?.data?.type, key);
})()
}
/>
</Form.Item>
)
}
if (config.type === 'memoryConfig') {
return (
<Form.Item
key={key}
name={key}
noStyle
>
<MemoryConfig
parentName={key}
options={getFilteredVariableList('llm')}
/>
</Form.Item>
)
}
if (config.type === 'conditionList') {
return (
<Form.Item
key={key}
name={key}
noStyle
>
<ConditionList
parentName={key}
options={(() => {
const cycleVars = values?.cycle_vars || [];
const cycleVarSuggestions: Suggestion[] = cycleVars.filter(vo => vo.name && vo.name.trim() !== '').map((cycleVar: any) => ({
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
label: cycleVar.name,
type: 'variable',
dataType: cycleVar.type || 'String',
value: `${selectedNode.getData().id}.${cycleVar.name}`,
nodeData: selectedNode.getData(),
}));
return [...getFilteredVariableList(selectedNode?.data?.type, key), ...cycleVarSuggestions];
})()}
selectedNode={selectedNode}
graphRef={graphRef}
addBtnText={t('workflow.config.addCase')}
/>
</Form.Item>
)
}
if (key === 'vision_input' && !values?.vision) {
return null
}
return (
<Form.Item
key={key}
name={key}
label={key === 'vision_input'
? undefined : key === 'parallel_count'
? <span className="rb:text-[10px] rb:text-[#5B6167] rb:leading-3.5 rb:-mb-1!">{t(`workflow.config.${selectedNode?.data?.type}.${key}`)}</span>
: t(`workflow.config.${selectedNode?.data?.type}.${key}`)
}
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
className={
key === 'parallel' && values?.parallel
? 'rb:mb-1!'
: key === 'vision' && values?.vision
? 'rb:mb-2!'
: key === 'group' && values?.group
? 'rb:mb-3!'
: ''
}
hidden={Boolean(config.hidden)}
>
{config.type === 'input'
? <Input placeholder={t('common.pleaseEnter')} />
: config.type === 'textarea'
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
: config.type === 'select'
? <Select
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
placeholder={t('common.pleaseSelect')}
/>
: config.type === 'switch'
? <Switch onChange={
key === 'group'
? () => { form.setFieldValue('group_variables', []) }
: key === 'vision'
? () => { form.setFieldValue('vision_input', undefined) }
: undefined
} />
: config.type === 'categoryList'
? <CategoryList
parentName={key}
selectedNode={selectedNode}
graphRef={graphRef}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
: config.type === 'inputNumber'
? <InputNumber
placeholder={t('common.pleaseEnter')}
className="rb:w-full!"
onChange={(value) => form.setFieldValue(key, value)}
/>
: config.type === 'slider'
? <RbSlider
min={config.min}
max={config.max}
step={config.step || 0.01}
isInput={true}
size="small"
/>
: config.type === 'editor'
? <Editor options={variableList} variant="outlined" size="small" placeholder={config.placeholder || t('common.pleaseEnter')} />
: null
}
</Form.Item>
)
})
: config.type === 'modelSelect'
? <ModelSelect
placeholder={t('common.pleaseSelect')}
params={config.params}
size="small"
className="rb:w-full!"
/>
: config.type === 'customSelect'
? <CustomSelect
placeholder={t('common.pleaseSelect')}
url={config.url as string}
params={config.params}
hasAll={false}
valueKey={config.valueKey}
labelKey={config.labelKey}
size="small"
/>
: config.type === 'variableList'
? <VariableSelect
placeholder={t(config.placeholder || 'common.pleaseSelect')}
options={(() => {
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
// Apply filtering if specified in config
if (config.filterNodeTypes || config.filterVariableNames) {
return baseVariableList.filter(variable => {
const nodeTypeMatch = !config.filterNodeTypes ||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
const variableNameMatch = !config.filterVariableNames ||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
return nodeTypeMatch || variableNameMatch;
});
}
if (config.onFilterVariableNames) {
return baseVariableList.filter(variable => Array.isArray(config.onFilterVariableNames) && config.onFilterVariableNames.includes(variable.label));
}
// Filter child nodes for iteration output
if (config.filterChildNodes && selectedNode) {
const graph = graphRef.current;
if (!graph) return [];
const nodes = graph.getNodes();
// Find child nodes whose cycle field equals parent node's ID
const childNodes = nodes.filter(node => {
const nodeData = node.getData();
return nodeData?.cycle === selectedNode.id;
});
return baseVariableList.filter(variable =>
childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
);
}
return baseVariableList;
})()}
onChange={(value, option) => handleChangeVariableList(value, option, key)}
size="small"
/>
: config.type === 'switch'
? <Switch onChange={
key === 'group'
? () => { form.setFieldValue('group_variables', []) }
: key === 'vision'
? () => { form.setFieldValue('vision_input', undefined) }
: undefined
} />
: config.type === 'categoryList'
? <CategoryList
parentName={key}
selectedNode={selectedNode}
graphRef={graphRef}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
/>
: config.type === 'editor'
? <Editor options={variableList} variant="outlined" size="small" placeholder={config.placeholder || t('common.pleaseEnter')} />
: null
}
</Form.Item>
)
})
}
</Form>
@@ -829,7 +837,7 @@ const Properties: FC<PropertiesProps> = ({
className={clsx("rb:size-3 rb:bg-cover rb:bg-[url('src/assets/images/common/caret_right_outlined.svg')]", {
'rb:rotate-90': !outputCollapsed
})}
></div>
></div>
</Flex>
{!outputCollapsed && currentNodeVariables.map(vo => (
<Flex key={vo.value} gap={4}>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:06:18
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-06 14:52:02
* @Last Modified time: 2026-03-07 17:10:59
*/
import LoopNode from './components/Nodes/LoopNode';
import NormalNode from './components/Nodes/NormalNode';
@@ -34,8 +34,6 @@ import memoryWriteIcon from '@/assets/images/workflow/memory-write.svg'
import unknownIcon from '@/assets/images/workflow/unknown.svg'
import { memoryConfigListUrl } from '@/api/memory'
import { getModelListUrl } from '@/api/models'
import type { NodeLibrary } from './types'
/**
@@ -104,8 +102,7 @@ export const nodeLibrary: NodeLibrary[] = [
config: {
model_id: {
type: 'define',
url: getModelListUrl,
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
params: { type: 'llm,chat' }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},
@@ -168,11 +165,8 @@ export const nodeLibrary: NodeLibrary[] = [
{ type: "parameter-extractor", icon: parameterExtractionIcon,
config: {
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
type: 'modelSelect',
params: { type: 'llm,chat' }, // llm/chat
},
text: {
type: 'variableList',
@@ -260,11 +254,8 @@ export const nodeLibrary: NodeLibrary[] = [
{ type: "question-classifier", icon: questionClassifierIcon,
config: {
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
type: 'modelSelect',
params: { type: 'llm,chat' }, // llm/chat
},
input_variable: {
type: 'variableList',