Merge pull request #858 from SuanmoSuanyangTechnology/feature/file_variable_zy
Feature/file variable zy
This commit is contained in:
@@ -1496,6 +1496,32 @@ export const en = {
|
||||
resetFeaturesTip: 'Please reconfigure the [Conversation Features - File Upload] settings',
|
||||
logTitle: 'Description',
|
||||
range: 'Range',
|
||||
body: 'BODY Parameter Example',
|
||||
bodyRequestExample: `{
|
||||
"message": "user message content",
|
||||
// string, required, the conversation content entered by the user;
|
||||
|
||||
"conversation_id": "conversation_id",
|
||||
// string, optional, session ID; for multi-turn conversations, pass the conversation_id from the previous response; omit on first request;
|
||||
|
||||
"user_id": "user_id",
|
||||
// string, optional, end-user identifier to distinguish memory and sessions across users; recommended to pass your business system user ID;
|
||||
|
||||
"variables": {},
|
||||
// object, optional (requires application configuration to take effect);
|
||||
|
||||
"stream": false,
|
||||
// boolean, optional, whether to stream the response; defaults to false; when true, returns an SSE event stream;
|
||||
|
||||
"thinking": false,
|
||||
// boolean, optional, whether to enable deep thinking; defaults to false (requires application configuration when true);
|
||||
|
||||
"files": [],
|
||||
// array, optional, list of multimodal attachments (requires application configuration to take effect);
|
||||
|
||||
"version":"app_release_id"
|
||||
// string, optional, application version ID; specify a historical release version ID, or omit to use the currently active version;
|
||||
}`,
|
||||
},
|
||||
userMemory: {
|
||||
userMemory: 'User Memory',
|
||||
@@ -2239,6 +2265,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
addvariable: 'Chat Variables',
|
||||
addChatVariable: 'Add Chat Variable',
|
||||
editChatVariable: 'Edit Chat Variable',
|
||||
invalidJSON: 'Invalid JSON format',
|
||||
|
||||
config: {
|
||||
llm: {
|
||||
@@ -2341,6 +2368,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
"eq": 'Is',
|
||||
"ne": 'Is Not',
|
||||
},
|
||||
file: {
|
||||
"empty": 'Not Exist',
|
||||
"not_empty": 'Exists',
|
||||
eq: 'All Are'
|
||||
},
|
||||
else_desc: 'Used to define the logic that should be executed when the if condition is not met.',
|
||||
unset: 'Condition Not Set',
|
||||
set: 'Set',
|
||||
@@ -2519,6 +2551,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'document-extractor.file_selector': 'File variable',
|
||||
'list-operator.input_list': 'Input list',
|
||||
},
|
||||
checkListHasErrors: 'Please resolve all issues in the checklist before publishing',
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: 'Emotion Engine Configuration',
|
||||
|
||||
@@ -831,6 +831,32 @@ export const zh = {
|
||||
resetFeaturesTip: '请重新配置【对话功能-文件上传】功能',
|
||||
logTitle: '描述',
|
||||
range: '范围',
|
||||
body: 'BODY 参数示例',
|
||||
bodyRequestExample: `{
|
||||
"message": "用户消息内容",
|
||||
// string,必填,用户输入的对话内容;
|
||||
|
||||
"conversation_id": "conversation_id",
|
||||
// string,可选,会话ID,多轮对话时传上一次返回的conversation_id,首次不传;
|
||||
|
||||
"user_id": "user_id",
|
||||
// string,可选,终端用户标识,用于区分不同用户的记忆和会话,建议传业务系统的用户ID;
|
||||
|
||||
"variables": {},
|
||||
// object,可选(需要应用配置才支持生效);
|
||||
|
||||
"stream": false,
|
||||
// boolean,可选,是否流式返回;默认 false,true时返回SSE事件流;
|
||||
|
||||
"thinking": false,
|
||||
// boolean,可选,是否启用深度思考;默认 false(true时需要应用配置才支持生效);
|
||||
|
||||
"files": [],
|
||||
// array,可选,多模态附件列表(需要应用配置才支持生效);
|
||||
|
||||
"version":"app_release_id"
|
||||
//string,可选,应用版本ID;指定历史发布版本ID,不传则使用当前生效版本;
|
||||
}`,
|
||||
},
|
||||
table: {
|
||||
totalRecords: '共 {{total}} 条记录'
|
||||
@@ -2200,6 +2226,7 @@ export const zh = {
|
||||
addvariable: '会话变量',
|
||||
addChatVariable: '添加会话变量',
|
||||
editChatVariable: '编辑会话变量',
|
||||
invalidJSON: 'JSON 格式不正确',
|
||||
|
||||
config: {
|
||||
llm: {
|
||||
@@ -2302,6 +2329,11 @@ export const zh = {
|
||||
"eq": '是',
|
||||
"ne": '不是',
|
||||
},
|
||||
file: {
|
||||
"empty": '不存在',
|
||||
"not_empty": '存在',
|
||||
eq: '全都是'
|
||||
},
|
||||
else_desc: '用于定义当 if 条件不满足时应执行的逻辑。',
|
||||
unset: '条件未设置',
|
||||
set: '已设置',
|
||||
@@ -2483,6 +2515,7 @@ export const zh = {
|
||||
'document-extractor.file_selector': '文件变量',
|
||||
'list-operator.input_list': '输入变量',
|
||||
},
|
||||
checkListHasErrors: '发布前确认检查清单中所有问题均已解决',
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: '情感引擎配置',
|
||||
|
||||
21
web/src/store/workflow.ts
Normal file
21
web/src/store/workflow.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-10 18:11:19
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-10 18:11:19
|
||||
*/
|
||||
import { create } from 'zustand'
|
||||
import type { NodeCheckResult } from '@/views/Workflow/components/CheckList'
|
||||
|
||||
interface WorkflowState {
|
||||
checkResults: Record<string, NodeCheckResult[]>
|
||||
setCheckResults: (appId: string, results: NodeCheckResult[]) => void
|
||||
getCheckResults: (appId: string) => NodeCheckResult[]
|
||||
}
|
||||
|
||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
checkResults: {},
|
||||
setCheckResults: (appId, results) =>
|
||||
set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })),
|
||||
getCheckResults: (appId) => get().checkResults[appId] ?? [],
|
||||
}))
|
||||
@@ -420,4 +420,7 @@ body {
|
||||
.ant-picker-outlined:focus,
|
||||
.ant-picker-outlined:focus-within {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ͼ1.cm-focused {
|
||||
outline: none;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:29
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-26 15:31:36
|
||||
* @Last Modified time: 2026-04-10 18:09:56
|
||||
*/
|
||||
import { type FC, useState, useRef, useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
@@ -18,6 +18,7 @@ import ApiKeyConfigModal from './components/ApiKeyConfigModal';
|
||||
import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey';
|
||||
import { maskApiKeys } from '@/utils/apiKeyReplacer'
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
import CodeMirrorEditor from '@/components/CodeMirrorEditor'
|
||||
|
||||
/**
|
||||
* API configuration page component
|
||||
@@ -155,6 +156,21 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<div className="rb:font-medium rb:mt-4!">
|
||||
{t('application.body')}
|
||||
</div>
|
||||
<Flex align="start" justify="space-between" className="rb:text-[#5B6167] rb:mt-3! rb:py-2! rb:px-4! rb:bg-white rb-border rb:rounded-lg rb:leading-5">
|
||||
<CodeMirrorEditor readOnly={true} value={t('application.bodyRequestExample')} />
|
||||
|
||||
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(t('application.bodyRequestExample'))}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
</RbCard>
|
||||
<RbCard
|
||||
title={() => (<Flex align="center">
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:41
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-26 15:24:41
|
||||
* @Last Modified time: 2026-04-10 17:02:07
|
||||
*/
|
||||
import { type FC, useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
import { Space, Input, Form, App, Flex } from 'antd';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
import Tag, { type TagProps } from './components/Tag'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
@@ -17,6 +18,7 @@ import ReleaseShareModal from './components/ReleaseShareModal'
|
||||
import AppSharingModal from './components/AppSharingModal'
|
||||
import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import { useWorkflowStore } from '@/store/workflow'
|
||||
import Empty from '@/components/Empty'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import Markdown from '@/components/Markdown'
|
||||
@@ -40,6 +42,7 @@ const heightClass = 'rb:max-h-[calc(100vh-140px)]'
|
||||
const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refresh}) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const { getCheckResults } = useWorkflowStore()
|
||||
const releaseModalRef = useRef<ReleaseModalRef>(null)
|
||||
const releaseShareModalRef = useRef<ReleaseShareModalRef>(null)
|
||||
const appSharingModalRef = useRef<AppSharingModalRef>(null)
|
||||
@@ -75,6 +78,10 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
if (!selectedVersion) return
|
||||
appExport(data.id, data.name, { release_id: selectedVersion.id})
|
||||
}
|
||||
const handleCopy = (id: string) => {
|
||||
copy(id)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
return (
|
||||
<Flex gap={12}>
|
||||
<div className="rb:w-101 rb:h-full">
|
||||
@@ -102,7 +109,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
</Tag>}
|
||||
</>}
|
||||
className={clsx("rb:hover:shadow-[0px_2px_8px_0px_rgba(0,0,0,0.2)]! rb:cursor-pointer rb:bg-white", {
|
||||
'rb:border-[#171719]!': version.id === selectedVersion.id,
|
||||
'rb:border! rb:border-[#171719]!': version.id === selectedVersion.id,
|
||||
'rb:border-[#DFE4ED] ': version.id !== selectedVersion.id
|
||||
})}
|
||||
headerType="borderless"
|
||||
@@ -140,13 +147,30 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
<RbButton type="primary" ghost onClick={() => releaseShareModalRef.current?.handleOpen()}>{t('application.share')}</RbButton>
|
||||
{data?.type !== 'multi_agent' && <RbButton type="primary" ghost onClick={() => appSharingModalRef.current?.handleOpen()}>{t('application.sharing')}</RbButton>}
|
||||
</>}
|
||||
<RbButton type="primary" onClick={() => releaseModalRef.current?.handleOpen()}>{t('application.release')}</RbButton>
|
||||
<RbButton type="primary" onClick={async () => {
|
||||
if (data?.type === 'workflow') {
|
||||
const errors = getCheckResults(data.id)
|
||||
if (errors.length) {
|
||||
message.error(t('workflow.checkListHasErrors'))
|
||||
return
|
||||
}
|
||||
}
|
||||
releaseModalRef.current?.handleOpen()
|
||||
}}>{t('application.release')}</RbButton>
|
||||
</Space>
|
||||
</Flex>
|
||||
{selectedVersion &&
|
||||
<Flex gap={16} vertical className={`${heightClass} rb:overflow-y-auto`}>
|
||||
<RbCard
|
||||
title={t('application.VersionInformation')}
|
||||
title={() => <Flex>{t('application.VersionInformation')}
|
||||
<Flex align="center" className="rb:text-[#5B6167] rb:text-[12px]">
|
||||
(ID: {selectedVersion.id}
|
||||
<div className="rb:size-4.5 rb:ml-1 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/copy_dark.svg')]"
|
||||
onClick={() => handleCopy(selectedVersion.id)}
|
||||
></div>
|
||||
)
|
||||
</Flex>
|
||||
</Flex>}
|
||||
headerType="borderless"
|
||||
>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
|
||||
|
||||
@@ -207,7 +207,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
</Flex>}
|
||||
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
|
||||
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
|
||||
<CheckList workflowRef={workflowRef} />
|
||||
<CheckList workflowRef={workflowRef} appId={application?.id ?? ''} />
|
||||
<Popover content={t('application.features')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
|
||||
<div
|
||||
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/features.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:34:04
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-31 15:35:13
|
||||
* @Last Modified time: 2026-04-10 16:32:52
|
||||
*/
|
||||
import { type FC, useState } from 'react'
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Divider, Flex } from 'antd'
|
||||
@@ -23,7 +23,6 @@ interface DataItem {
|
||||
const ConversationMemory: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
@@ -32,14 +31,12 @@ const ConversationMemory: FC = () => {
|
||||
headerClassName="rb:min-h-[54px]! rb:pt-0! rb:mb-0!"
|
||||
bodyClassName="rb:p-4! rb:pt-0! rb:pb-1! rb:h-[calc(100%-54px)]!"
|
||||
className="rb:h-full!"
|
||||
extra={<div className="rb:text-[#5B6167] rb:leading-5">{t('userMemory.totalRagMemory')}: <span className="rb:font-medium rb:text-[#171719]">{total}</span></div>}
|
||||
>
|
||||
<PageScrollList<DataItem>
|
||||
url={getRagContentUrl}
|
||||
query={{ end_user_id: id }}
|
||||
column={1}
|
||||
gutter={0}
|
||||
onTotalChange={setTotal}
|
||||
renderItem={(item, index) => (
|
||||
<div>
|
||||
{index !== 0 && <Divider className="rb:mt-1! rb:mb-3! rb:ml-11!" />}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @Last Modified time: 2026-04-08 11:05:34
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState, useRef, useMemo } from 'react';
|
||||
import { Form, Input, Select, InputNumber, Button, Row, Col, Flex, Spin } from 'antd';
|
||||
import { Form, Input, Select, InputNumber, Button, Row, Col, Flex } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -124,7 +124,7 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
setFileList(list);
|
||||
}
|
||||
} else if (variable.type.includes('object') && variable.defaultValue) {
|
||||
form.setFieldValue('defaultValue', JSON.stringify(variable.defaultValue, null, 2))
|
||||
form.setFieldValue('defaultValue', variable.defaultValue ? JSON.stringify(variable.defaultValue, null, 2) : undefined)
|
||||
}
|
||||
} else {
|
||||
form.resetFields();
|
||||
@@ -342,7 +342,19 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
</Form.Item>
|
||||
)
|
||||
: (
|
||||
<Form.Item name="defaultValue" label={t('workflow.config.parameter-extractor.default')}>
|
||||
<Form.Item
|
||||
name="defaultValue"
|
||||
label={t('workflow.config.parameter-extractor.default')}
|
||||
rules={[
|
||||
(type === 'object' || type === 'array[object]') ? {
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve();
|
||||
try { JSON.parse(value); return Promise.resolve(); }
|
||||
catch { return Promise.reject(t('workflow.invalidJSON')); }
|
||||
}
|
||||
} : {}
|
||||
]}
|
||||
>
|
||||
{type === 'number'
|
||||
? <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />
|
||||
: type === 'boolean'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { useState, useCallback, useEffect, useRef, type FC } from 'react'
|
||||
import { Popover, Flex } from 'antd'
|
||||
import { WarningFilled } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -8,17 +8,19 @@ import type { WorkflowRef } from '@/views/ApplicationConfig/types'
|
||||
import { nodeLibrary } from '../../constant'
|
||||
import { getToolMethods } from '@/api/tools'
|
||||
import RbDrawer from '@/components/RbDrawer'
|
||||
import { useWorkflowStore } from '@/store/workflow'
|
||||
|
||||
interface CheckListProps {
|
||||
workflowRef: React.RefObject<WorkflowRef>
|
||||
appId: string
|
||||
}
|
||||
|
||||
interface CheckError {
|
||||
export interface CheckError {
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
|
||||
interface NodeCheckResult {
|
||||
export interface NodeCheckResult {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
@@ -112,10 +114,67 @@ function validateNode(type: string, config: Record<string, any>): CheckError[] {
|
||||
return errors
|
||||
}
|
||||
|
||||
const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
|
||||
export async function runCheckOnGraph(
|
||||
graph: import('@antv/x6').Graph,
|
||||
t: (key: string) => string
|
||||
): Promise<NodeCheckResult[]> {
|
||||
const nodes = graph.getNodes()
|
||||
const edges = graph.getEdges()
|
||||
const targetIds = new Set<string>()
|
||||
const childTargetIds = new Set<string>()
|
||||
edges.forEach(e => {
|
||||
targetIds.add(e.getTargetCellId())
|
||||
const srcData = graph.getCellById(e.getSourceCellId())?.getData()
|
||||
const tgtData = graph.getCellById(e.getTargetCellId())?.getData()
|
||||
if (srcData?.cycle && tgtData?.cycle && srcData.cycle === tgtData.cycle) {
|
||||
childTargetIds.add(e.getTargetCellId())
|
||||
}
|
||||
})
|
||||
|
||||
const checked: NodeCheckResult[] = []
|
||||
for (const node of nodes) {
|
||||
const data = node.getData()
|
||||
if (!data || ['add-node', 'notes', 'cycle-start', 'break'].includes(data.type)) continue
|
||||
|
||||
const errors: CheckError[] = []
|
||||
const isChildNode = !!data.cycle
|
||||
const hasIncoming = isChildNode ? childTargetIds.has(node.id) : !['start', 'cycle-start'].includes(data.type) ? targetIds.has(node.id) : true
|
||||
if (!hasIncoming) errors.push({ key: 'notConnected', message: t('workflow.notConnected') })
|
||||
|
||||
const configErrors = validateNode(data.type, data.config ?? {})
|
||||
configErrors.forEach(e => {
|
||||
errors.push({ key: e.key, message: `${t(`workflow.checkListErrors.${e.key}`)} ${t('workflow.cannotBeEmpty')}`.trim() })
|
||||
})
|
||||
|
||||
if (data.type === 'tool') {
|
||||
const toolId = data.config?.tool_id?.defaultValue ?? data.config?.tool_id
|
||||
const toolParameters = data.config?.tool_parameters?.defaultValue ?? data.config?.tool_parameters ?? {}
|
||||
if (toolId) {
|
||||
try {
|
||||
const methods = await getToolMethods(toolId) as Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }>
|
||||
const operation = toolParameters?.operation
|
||||
const method = operation ? methods.find(m => m.name === operation) : methods[0]
|
||||
if (method) {
|
||||
method.parameters
|
||||
.filter(p => p.required && (toolParameters[p.name] === undefined || toolParameters[p.name] === null || toolParameters[p.name] === ''))
|
||||
.forEach(p => errors.push({ key: 'tool.tool_parameters', message: `${p.name} ${t('workflow.cannotBeEmpty')}` }))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
checked.push({ id: node.id, name: data.name || t(`workflow.${data.type}`), type: data.type, icon: nodeIconMap[data.type] ?? '', errors })
|
||||
}
|
||||
}
|
||||
return checked
|
||||
}
|
||||
|
||||
const CheckList: FC<CheckListProps> = ({ workflowRef, appId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [results, setResults] = useState<NodeCheckResult[]>([])
|
||||
const { setCheckResults, getCheckResults } = useWorkflowStore()
|
||||
const results = getCheckResults(appId)
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
|
||||
const runCheck = useCallback(async () => {
|
||||
@@ -195,7 +254,7 @@ const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
|
||||
const scheduleCheck = useCallback(() => {
|
||||
clearTimeout(timerRef.current)
|
||||
timerRef.current = setTimeout(async () => {
|
||||
setResults(await runCheck())
|
||||
setCheckResults(appId, await runCheck())
|
||||
}, 500)
|
||||
}, [runCheck])
|
||||
|
||||
@@ -211,7 +270,7 @@ const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
|
||||
}
|
||||
}, [workflowRef.current?.graphRef?.current])
|
||||
|
||||
const handleOpen = () => {
|
||||
const handleOpen = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -328,7 +328,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
||||
};
|
||||
|
||||
const content = (
|
||||
<Flex vertical gap={16} className="rb:max-h-[300px] rb:overflow-y-auto rb:p-3" style={{ minWidth: `${nodeWidth}px` }}>
|
||||
<Flex vertical gap={16} className="rb:max-h-75 rb:overflow-y-auto rb:p-3" style={{ minWidth: `${nodeWidth}px` }}>
|
||||
{nodeLibrary.map((category) => {
|
||||
const sourceNodeData = sourceNode?.getData();
|
||||
const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-25 15:23:45
|
||||
*/
|
||||
import { type FC } from 'react'
|
||||
import { useMemo, type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Divider, InputNumber, type SelectProps, Flex, Row, Col } from 'antd'
|
||||
@@ -15,7 +15,7 @@ import Editor from '../../Editor'
|
||||
import { edgeAttrs, nodeWidth } from '../../../constant'
|
||||
import RbButton from '@/components/RbButton';
|
||||
import RadioGroupBtn from '../RadioGroupBtn'
|
||||
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils'
|
||||
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils';
|
||||
|
||||
interface CaseListProps {
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
|
||||
@@ -49,6 +49,34 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = {
|
||||
boolean: [
|
||||
{ value: 'eq', label: 'workflow.config.if-else.boolean.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.if-else.boolean.ne' },
|
||||
],
|
||||
object: [
|
||||
{ value: 'eq', label: 'workflow.config.if-else.boolean.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.if-else.boolean.ne' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
],
|
||||
file: [
|
||||
{ value: 'empty', label: 'workflow.config.if-else.file.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.file.not_empty' },
|
||||
],
|
||||
// TODO:包含、不包含、全都是
|
||||
'array[file]': [
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
// { value: 'eq', label: 'workflow.config.if-else.eq' },
|
||||
// { value: 'contains', label: 'workflow.config.if-else.contains' },
|
||||
// { value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
|
||||
],
|
||||
'array': [
|
||||
{ value: 'contains', label: 'workflow.config.if-else.contains' },
|
||||
{ value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
],
|
||||
'array[object]': [
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -247,6 +275,22 @@ const CaseList: FC<CaseListProps> = ({
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], undefined);
|
||||
};
|
||||
|
||||
const filterNumberOptions = useMemo(() => {
|
||||
const filterList: Suggestion[] = []
|
||||
options.forEach(vo => {
|
||||
if (vo.children && vo.children?.length > 0) {
|
||||
filterList.push({
|
||||
...vo,
|
||||
children: vo.children.filter(child => child.dataType === 'number')
|
||||
})
|
||||
} else if (vo.dataType === 'number') {
|
||||
filterList.push(vo)
|
||||
}
|
||||
})
|
||||
|
||||
return filterList
|
||||
}, [options])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.List name={name}>
|
||||
@@ -284,11 +328,15 @@ const CaseList: FC<CaseListProps> = ({
|
||||
const currentCase = cases[caseIndex] || {};
|
||||
const currentExpression = currentCase.expressions?.[conditionIndex] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || leftFieldType === 'file' || leftFieldType === 'array[object]' || leftFieldType === 'array[file]';
|
||||
const operatorList = leftFieldType && operatorsObj[leftFieldType]
|
||||
? operatorsObj[leftFieldType]
|
||||
: leftFieldType && leftFieldType?.includes('array')
|
||||
? operatorsObj.array
|
||||
: operatorsObj.default;
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
return (
|
||||
<Flex key={conditionField.key} gap={4} align="start" className="rb:mb-2!">
|
||||
@@ -312,7 +360,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
<Col flex="1">
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
options={(operatorList ?? []).map(vo => ({
|
||||
...vo,
|
||||
label: t(String(vo?.label || ''))
|
||||
}))}
|
||||
@@ -328,7 +376,9 @@ const CaseList: FC<CaseListProps> = ({
|
||||
|
||||
{!hideRightField && (
|
||||
<div className="rb:py-1 rb:px-1.5">
|
||||
{leftFieldType === 'number'
|
||||
{leftFieldType === 'array[file]'
|
||||
? <>TODO</>
|
||||
: leftFieldType === 'number'
|
||||
? <Flex align="center">
|
||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
@@ -345,24 +395,24 @@ const CaseList: FC<CaseListProps> = ({
|
||||
{inputType === 'variable'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
options={filterNumberOptions}
|
||||
allowClear={false}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
/>
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
|
||||
/>
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
: (
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <RadioGroupBtn options={[ { value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
|
||||
{['boolean', 'array[boolean]'].includes(leftFieldType as string)
|
||||
? <RadioGroupBtn options={[{ value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
|
||||
: <Editor options={options} size="small" type="input" />
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC } from 'react'
|
||||
import { type FC, useMemo } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, InputNumber, Input, Divider, type SelectProps, Flex, Space, Row, Col } from 'antd'
|
||||
@@ -47,6 +47,18 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = {
|
||||
{ value: 'ne', label: 'workflow.config.if-else.boolean.ne' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
],
|
||||
// 为空、不为空
|
||||
object: [
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
],
|
||||
// 包含、不包含、为空、不为空
|
||||
'array': [
|
||||
{ value: 'contains', label: 'workflow.config.if-else.contains' },
|
||||
{ value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -81,6 +93,23 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
const currentValue = form.getFieldValue([parentName, 'logical_operator']);
|
||||
form.setFieldValue([parentName, 'logical_operator'], currentValue === 'and' ? 'or' : 'and');
|
||||
};
|
||||
|
||||
const getNumVariable = useMemo(() => {
|
||||
const filterList: Suggestion[] = []
|
||||
options.forEach(variable => {
|
||||
if (variable.dataType === 'number') {
|
||||
filterList.push(variable)
|
||||
} else if (variable.dataType === 'file') {
|
||||
filterList.push({
|
||||
...variable,
|
||||
disabled: true,
|
||||
children: variable.children?.filter(child => child.dataType === 'number')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return filterList
|
||||
}, [options])
|
||||
return (
|
||||
<>
|
||||
<Form.List name={[parentName, 'expressions']}>
|
||||
@@ -125,11 +154,19 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
|
||||
const currentExpression = expressions[index] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || ['array[object]', 'object'].includes(leftFieldType as string);
|
||||
const operatorList = leftFieldType && ['array[object]', 'object'].includes(leftFieldType)
|
||||
? operatorsObj.object
|
||||
: leftFieldType && ['array[boolean]', 'boolean'].includes(leftFieldType)
|
||||
? operatorsObj.boolean
|
||||
: leftFieldType && operatorsObj[leftFieldType]
|
||||
? operatorsObj[leftFieldType]
|
||||
: leftFieldType?.includes('array')
|
||||
? operatorsObj.array
|
||||
: operatorsObj.default
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
return (
|
||||
<Flex
|
||||
@@ -146,10 +183,11 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
<Form.Item name={[field.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
options={options.filter(vo =>
|
||||
vo.value.includes('sys.') ||
|
||||
!['file', 'array[file]'].includes(vo.dataType) &&
|
||||
(vo.value.includes('sys.') ||
|
||||
vo.value.includes('conv.') ||
|
||||
vo.nodeData.type === 'loop' ||
|
||||
(vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id)
|
||||
(vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id))
|
||||
)}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
@@ -163,7 +201,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
<Col flex="96px">
|
||||
<Form.Item name={[field.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
options={(operatorList??[]).map(vo => ({
|
||||
...vo,
|
||||
label: t(String(vo?.label || ''))
|
||||
}))}
|
||||
@@ -198,7 +236,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
? (
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
options={getNumVariable}
|
||||
allowClear={false}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
@@ -219,7 +257,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
: (
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <RadioGroupBtn options={[ { value: true, label: 'True' }, { value: false, label: 'False' }]} />
|
||||
? <RadioGroupBtn options={[ { value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
|
||||
: <Input variant="borderless" placeholder={t('common.pleaseEnter')} />
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
@@ -6,6 +6,7 @@ import VariableSelect from '../VariableSelect'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import RadioGroupBtn from '../RadioGroupBtn'
|
||||
import { getChildNodeVariables } from '../hooks/useVariableList'
|
||||
import CodeMirrorEditor from '@/components/CodeMirrorEditor';
|
||||
|
||||
interface CycleVar {
|
||||
name: string;
|
||||
@@ -28,11 +29,17 @@ const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'object',
|
||||
'array[string]',
|
||||
'array[number]',
|
||||
'array[boolean]',
|
||||
'array[object]'
|
||||
]
|
||||
const object_placeholder = `# example
|
||||
# {
|
||||
# "name": "redbear",
|
||||
# "age": 2
|
||||
# }`
|
||||
|
||||
const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
value = [],
|
||||
@@ -144,6 +151,13 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
{ value: true, label: 'True' },
|
||||
{ value: false, label: 'False' }]}
|
||||
/>
|
||||
: currentType === 'object'
|
||||
? <CodeMirrorEditor
|
||||
language="json"
|
||||
placeholder={object_placeholder}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
: (
|
||||
<Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
|
||||
@@ -38,12 +38,44 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
...(typeOptions.length > 0 && { type: typeOptions[0].value })
|
||||
});
|
||||
|
||||
// Filter options based on boolean type if needed
|
||||
const booleanFilterOptions = useMemo(() => {
|
||||
return filterBooleanType
|
||||
? options.filter(option => option.dataType !== 'boolean')
|
||||
: options
|
||||
}, [options, filterBooleanType])
|
||||
const namefilterOptions = useMemo(() => {
|
||||
const filterList: Suggestion[] = [];
|
||||
options.forEach(vo => {
|
||||
if (vo.dataType === 'file') {
|
||||
filterList.push({
|
||||
...vo,
|
||||
disabled: true,
|
||||
children: vo.children?.filter(child => child.dataType !== 'boolean')
|
||||
})
|
||||
} else if (vo.dataType !== 'array[file]') {
|
||||
filterList.push(vo)
|
||||
}
|
||||
})
|
||||
|
||||
return filterList
|
||||
}, [options])
|
||||
const valueFilterOptions = (type?: string) => {
|
||||
let filterOptions: Suggestion[] = []
|
||||
options.forEach(vo => {
|
||||
if (type === 'file' && vo.dataType === 'file') {
|
||||
filterOptions.push({
|
||||
...vo,
|
||||
children: []
|
||||
})
|
||||
} else if (type === 'file' && vo.dataType === 'array[file]') {
|
||||
filterOptions.push(vo)
|
||||
} else if (vo.dataType === 'file') {
|
||||
filterOptions.push({
|
||||
...vo,
|
||||
disabled: true
|
||||
})
|
||||
} else if (vo.dataType !== 'array[file]') {
|
||||
filterOptions.push(vo)
|
||||
}
|
||||
})
|
||||
|
||||
return filterOptions
|
||||
}
|
||||
|
||||
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
|
||||
const hasType = typeOptions.length > 0;
|
||||
@@ -57,7 +89,7 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item name={[index, 'name']} className={formClassName}>
|
||||
<Editor
|
||||
options={booleanFilterOptions.filter(option => !option.dataType.includes('file'))}
|
||||
options={namefilterOptions}
|
||||
type="input"
|
||||
className={contentClassName}
|
||||
size={size}
|
||||
@@ -105,9 +137,7 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
>
|
||||
{(form) => {
|
||||
const currentType = form.getFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'type']);
|
||||
const filteredOptions = currentType === 'file'
|
||||
? booleanFilterOptions.filter(option => option.dataType.includes('file'))
|
||||
: booleanFilterOptions.filter(option => !option.dataType.includes('file'));
|
||||
const filteredOptions = valueFilterOptions(currentType)
|
||||
|
||||
return (
|
||||
<Form.Item name={[index, 'value']} className={formClassName}>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-02 17:17:06
|
||||
*/
|
||||
import { type FC, useRef, useState } from "react";
|
||||
import { type FC, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input, Flex, Radio } from 'antd'
|
||||
import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
@@ -84,6 +84,64 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
setCollapsed((prev: boolean) => !prev)
|
||||
}
|
||||
|
||||
const filterVariables = useMemo(() => {
|
||||
const filterList: Suggestion[] = []
|
||||
options.forEach(variable => {
|
||||
if (['number', 'string'].includes(variable.dataType)) {
|
||||
filterList.push(variable)
|
||||
} else if (variable.dataType === 'file') {
|
||||
filterList.push({
|
||||
...variable,
|
||||
disabled: true,
|
||||
children: variable.children?.filter(child => ['number', 'string'].includes(child.dataType))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return filterList
|
||||
}, [options])
|
||||
const filterVariablesWithFile = useMemo(() => {
|
||||
const filterList: Suggestion[] = []
|
||||
options.forEach(variable => {
|
||||
if (['number', 'string', 'file', 'array[file]'].includes(variable.dataType)) {
|
||||
filterList.push(variable)
|
||||
}
|
||||
})
|
||||
|
||||
return filterList
|
||||
}, [options])
|
||||
const jsonRawFilterVariables = useMemo(() => {
|
||||
const filterList: Suggestion[] = []
|
||||
options.forEach(variable => {
|
||||
if (['number', 'string', 'array[string]', 'array[number]'].includes(variable.dataType)) {
|
||||
filterList.push(variable)
|
||||
} else if (variable.dataType === 'file') {
|
||||
filterList.push({
|
||||
...variable,
|
||||
disabled: true,
|
||||
children: variable.children?.filter(child => ['number', 'string', 'file', 'array[string]', 'array[number]'].includes(child.dataType))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return filterList
|
||||
}, [options])
|
||||
const fileFilterVariables = useMemo(() => {
|
||||
const filterList: Suggestion[] = []
|
||||
options.forEach(variable => {
|
||||
if (['array[file]'].includes(variable.dataType)) {
|
||||
filterList.push(variable)
|
||||
} else if (variable.dataType === 'file') {
|
||||
filterList.push({
|
||||
...variable,
|
||||
children: []
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return filterList
|
||||
}, [options])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex align="center" justify="space-between" className="rb:mb-1!">
|
||||
@@ -117,7 +175,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<Form.Item name="url">
|
||||
<Editor
|
||||
key="url"
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
options={filterVariables}
|
||||
variant="outlined"
|
||||
type="input"
|
||||
size="small"
|
||||
@@ -134,7 +192,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
size="small"
|
||||
parentName="headers"
|
||||
title="HEADERS"
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
options={filterVariables}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -143,7 +201,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
size="small"
|
||||
parentName="params"
|
||||
title="PARAMS"
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
options={filterVariables}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -167,7 +225,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName={['body', 'data']}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number' || vo.dataType.includes('file'))}
|
||||
options={filterVariablesWithFile}
|
||||
typeOptions={[
|
||||
{ label: 'text', value: 'text' },
|
||||
{ label: 'file', value: 'file' }
|
||||
@@ -180,7 +238,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName={['body', 'data']}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
options={filterVariablesWithFile}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -190,7 +248,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<MessageEditor
|
||||
key="json"
|
||||
parentName={['body', 'data']}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
options={jsonRawFilterVariables}
|
||||
isArray={false}
|
||||
title="JSON"
|
||||
titleVariant="borderless"
|
||||
@@ -204,7 +262,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<MessageEditor
|
||||
key="raw"
|
||||
parentName={['body', 'data']}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
options={jsonRawFilterVariables}
|
||||
isArray={false}
|
||||
title="RAW TEXT"
|
||||
titleVariant="borderless"
|
||||
@@ -220,7 +278,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<Editor
|
||||
key={['body', 'data'].join('_')}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType.includes('file'))}
|
||||
options={fileFilterVariables}
|
||||
type="input"
|
||||
size="small"
|
||||
height={28}
|
||||
|
||||
@@ -163,25 +163,45 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
|
||||
form.setFieldsValue(inititalValue)
|
||||
}
|
||||
const getNumberOptions = useMemo(() => {
|
||||
const list: Suggestion[] = []
|
||||
|
||||
// string -> string
|
||||
// integer -> number
|
||||
// number -> number
|
||||
// boolean -> boolean【只能选true/false】
|
||||
// array -> array[file]/array[object]/array[string]/array[number]/array[boolean]
|
||||
// object -> object/file
|
||||
const getFilterOptions = (type: string) => {
|
||||
const filterList: Suggestion[] = [];
|
||||
options.forEach(vo => {
|
||||
if (vo.children && vo?.children?.length > 0) {
|
||||
const filterChild = vo.children.filter(child => child.dataType === 'number')
|
||||
if (vo.children && vo.children?.length > 0) {
|
||||
const childOptions = vo.children?.filter(child => child.dataType === type || (type === 'integer' && child.dataType === 'number'))
|
||||
|
||||
if (filterChild.length > 0) {
|
||||
list.push({ ...vo, disabled: vo.dataType !== 'number', children: filterChild })
|
||||
} else if (vo.dataType === 'number') {
|
||||
list.push({ ...vo, children: [] })
|
||||
if (vo.dataType === type
|
||||
|| (type === 'integer' && vo.dataType === 'number')
|
||||
|| (type === 'array' && vo.dataType.includes(type))
|
||||
|| (type === 'object' && vo.dataType === 'object')
|
||||
) {
|
||||
filterList.push({
|
||||
...vo,
|
||||
children: childOptions
|
||||
})
|
||||
} else if (childOptions.length > 0) {
|
||||
filterList.push({
|
||||
...vo,
|
||||
disabled: true,
|
||||
children: childOptions
|
||||
})
|
||||
}
|
||||
} else if (vo.dataType === 'number') {
|
||||
list.push({ ...vo })
|
||||
} else if (vo.dataType === type
|
||||
|| (type === 'integer' && vo.dataType === 'number')
|
||||
|| (type === 'array' && vo.dataType.includes(type))
|
||||
|| (type === 'object' && vo.dataType === 'object')) {
|
||||
filterList.push(vo)
|
||||
}
|
||||
})
|
||||
console.log('options', options, list)
|
||||
return list
|
||||
}, [options])
|
||||
|
||||
return filterList
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -205,7 +225,7 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
<Form.Item
|
||||
name={['tool_parameters', parameter.name]}
|
||||
label={<>
|
||||
{parameter.name}
|
||||
{parameter.name} <span className="rb:text-[#5B6167] rb:mx-1">({parameter.type})</span>
|
||||
<Tooltip title={parameter.description} placement="right">
|
||||
<div className="rb:size-3 rb:ml-0.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/question.svg')]"></div>
|
||||
</Tooltip>
|
||||
@@ -220,21 +240,12 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
? <Select size="small" options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||
: parameter.type === 'boolean'
|
||||
? <Switch size="small" />
|
||||
: parameter.type === 'integer' || parameter.type === 'number'
|
||||
? <Editor
|
||||
variant="outlined"
|
||||
type="input"
|
||||
size="small"
|
||||
height={28}
|
||||
options={getNumberOptions}
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
/>
|
||||
: <Editor
|
||||
variant="outlined"
|
||||
type="input"
|
||||
size="small"
|
||||
height={28}
|
||||
options={options}
|
||||
options={getFilterOptions(parameter.type)}
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:39:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-08 14:10:40
|
||||
* @Last Modified time: 2026-04-10 17:24:19
|
||||
*/
|
||||
import { type FC, useEffect, useState, useMemo } from "react";
|
||||
import clsx from 'clsx'
|
||||
@@ -248,7 +248,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
return null;
|
||||
})() : null;
|
||||
|
||||
let filteredList = variableList.filter(variable => variable.dataType !== 'boolean');
|
||||
let filteredList = variableList.filter(variable => !['boolean', 'object', 'array[boolean]'].includes(variable.dataType));
|
||||
|
||||
// If this LLM node is a child of iteration/loop, ensure parent variables are included
|
||||
if (parentLoopNode) {
|
||||
@@ -315,23 +315,103 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
return filteredList;
|
||||
}
|
||||
if (nodeType === 'knowledge-retrieval' || nodeType === 'parameter-extractor' && key !== 'prompt' || nodeType === 'memory-read' || nodeType === 'question-classifier') {
|
||||
if (nodeType === 'knowledge-retrieval') {
|
||||
const allList = addParentIterationVars(variableList);
|
||||
let filteredList: Suggestion[] = []
|
||||
allList.forEach(variable => {
|
||||
if (variable.dataType === 'string') {
|
||||
filteredList.push(variable)
|
||||
} else if (variable.dataType === 'file') {
|
||||
filteredList.push({
|
||||
...variable,
|
||||
disabled: true,
|
||||
children: variable.children.filter((child: Suggestion) => child.dataType === 'string')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return filteredList
|
||||
}
|
||||
if ((nodeType === 'parameter-extractor' && key === 'text')
|
||||
|| (nodeType === 'question-classifier' && ['input_variable', 'categories'].includes(key as string))
|
||||
) {
|
||||
const allList = addParentIterationVars(variableList);
|
||||
let filteredList: Suggestion[] = []
|
||||
allList.forEach(variable => {
|
||||
if (variable.dataType === 'string') {
|
||||
filteredList.push(variable)
|
||||
} else if (variable.dataType === 'file') {
|
||||
filteredList.push({
|
||||
...variable,
|
||||
children: variable.children.filter((child: Suggestion) => child.dataType === 'string')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return filteredList
|
||||
}
|
||||
|
||||
if ((nodeType === 'parameter-extractor' && key === 'prompt')
|
||||
|| (nodeType === 'question-classifier' && key === 'user_supplement_prompt')
|
||||
) {
|
||||
const allList = addParentIterationVars(variableList);
|
||||
let filteredList: Suggestion[] = []
|
||||
allList.forEach(variable => {
|
||||
if (['string', 'number'].includes(variable.dataType)) {
|
||||
filteredList.push(variable)
|
||||
} else if (variable.dataType === 'file') {
|
||||
filteredList.push({
|
||||
...variable,
|
||||
disabled: true,
|
||||
children: variable.children.filter((child: Suggestion) => ['string', 'number'].includes(child.dataType))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return filteredList
|
||||
}
|
||||
if (nodeType === 'memory-read') {
|
||||
let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string');
|
||||
return filteredList;
|
||||
}
|
||||
if (nodeType === 'memory-write') {
|
||||
let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string' || variable.dataType.includes('file'));
|
||||
return filteredList;
|
||||
const allList = addParentIterationVars(variableList);
|
||||
let filteredList: Suggestion[] = []
|
||||
allList.forEach(variable => {
|
||||
if (['string', 'array[file]'].includes(variable.dataType)) {
|
||||
filteredList.push(variable)
|
||||
} else if (variable.dataType === 'file') {
|
||||
filteredList.push({
|
||||
...variable,
|
||||
children: variable.children.filter((child: Suggestion) => child.dataType === 'string')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return filteredList
|
||||
}
|
||||
if (nodeType === 'parameter-extractor' && key === 'prompt') {
|
||||
let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string' || variable.dataType === 'number');
|
||||
return filteredList;
|
||||
}
|
||||
if (nodeType === 'iteration' && key === 'output' || nodeType === 'loop' && key === 'condition') {
|
||||
|
||||
if ((nodeType === 'iteration' && key === 'output')) {
|
||||
if (!selectedNode) return [];
|
||||
let filteredList = nodeType === 'iteration'
|
||||
? variableList.filter(variable => variable.value.includes('sys.'))
|
||||
: addParentIterationVars(variableList).filter(variable => variable.nodeData.type !== 'loop');
|
||||
let filteredList = variableList.filter(variable => variable.value.includes('sys.'))
|
||||
const childVariables = getChildNodeVariables(selectedNode, graphRef);
|
||||
const existingKeys = new Set(filteredList.map(v => v.key));
|
||||
childVariables.forEach(v => {
|
||||
if (!existingKeys.has(v.key)) {
|
||||
filteredList.push(v);
|
||||
existingKeys.add(v.key);
|
||||
}
|
||||
});
|
||||
|
||||
return filteredList.filter(variable => variable.dataType !== 'array[file]');
|
||||
}
|
||||
if (nodeType === 'loop' && key === 'condition') {
|
||||
if (!selectedNode) return [];
|
||||
let filteredList = addParentIterationVars(variableList).filter(variable => variable.nodeData.type !== 'loop');
|
||||
|
||||
const childVariables = getChildNodeVariables(selectedNode, graphRef);
|
||||
const existingKeys = new Set(filteredList.map(v => v.key));
|
||||
@@ -348,6 +428,23 @@ const Properties: FC<PropertiesProps> = ({
|
||||
return variableList.filter(variable => variable.dataType.includes('array'));
|
||||
}
|
||||
|
||||
if ((nodeType === 'if-else' && key === 'cases')) {
|
||||
const allList = addParentIterationVars(variableList);
|
||||
let filteredList: Suggestion[] = []
|
||||
allList.forEach(variable => {
|
||||
if (variable.dataType === 'file') {
|
||||
filteredList.push({
|
||||
...variable,
|
||||
disabled: true,
|
||||
})
|
||||
} else {
|
||||
filteredList.push(variable)
|
||||
}
|
||||
})
|
||||
|
||||
return filteredList
|
||||
}
|
||||
|
||||
// For all other node types, add parent iteration variables if applicable
|
||||
let baseList = variableList;
|
||||
return addParentIterationVars(baseList);
|
||||
@@ -848,7 +945,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
: config.type === 'editor'
|
||||
? <Editor options={variableList} variant="outlined" size="small" placeholder={config.placeholder || t('common.pleaseEnter')} />
|
||||
? <Editor options={getFilteredVariableList(selectedNode?.data?.type, key)} variant="outlined" size="small" placeholder={config.placeholder || t('common.pleaseEnter')} />
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user