Merge pull request #420 from SuanmoSuanyangTechnology/feature/workflow_import_zy
feat(web): workflow import & export
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 13:59:45
|
* @Date: 2026-02-03 13:59:45
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-03 13:59:45
|
* @Last Modified time: 2026-02-28 16:34:15
|
||||||
*/
|
*/
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
|
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
|
||||||
@@ -120,3 +120,15 @@ export const copyApplication = (app_id: string, new_name: string) => {
|
|||||||
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
|
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
|
||||||
return request.get(`/apps/${app_id}/statistics`, data)
|
return request.get(`/apps/${app_id}/statistics`, data)
|
||||||
}
|
}
|
||||||
|
// 导出工作流
|
||||||
|
export const exportWorkflow = (app_id: string, fileName: string) => {
|
||||||
|
return request.downloadFile(`/apps/${app_id}/workflow/export`, fileName, undefined, undefined, 'GET')
|
||||||
|
}
|
||||||
|
// 工作流上传+兼容性分析
|
||||||
|
export const importWorkflow = (formData: FormData) => {
|
||||||
|
return request.uploadFile(`/apps/workflow/import`, formData)
|
||||||
|
}
|
||||||
|
// 完成工作流导入
|
||||||
|
export const completeImportWorkflow = (data: { temp_id: string; name?: string; description?: string }) => {
|
||||||
|
return request.post(`/apps/workflow/import/save`, data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1340,12 +1340,20 @@ export const en = {
|
|||||||
dynamicMatchSkill: 'Dynamic Match Skill',
|
dynamicMatchSkill: 'Dynamic Match Skill',
|
||||||
executeTask: 'Execute Task',
|
executeTask: 'Execute Task',
|
||||||
|
|
||||||
|
importWorkflow: 'Import Workflow',
|
||||||
|
platform: 'Source Platform',
|
||||||
upload: 'Upload & Parse',
|
upload: 'Upload & Parse',
|
||||||
complex: 'Compatibility Analysis',
|
complex: 'Compatibility Analysis',
|
||||||
node: 'Node Mapping',
|
|
||||||
configCheck: 'Configuration Validation',
|
|
||||||
sureInfo: 'Information Confirmation',
|
sureInfo: 'Information Confirmation',
|
||||||
completed: 'Import Completed',
|
completed: 'Import Completed',
|
||||||
|
workflowName: 'Workflow Name',
|
||||||
|
fileName: 'File Name',
|
||||||
|
fileSize: 'File Size',
|
||||||
|
importSuccess: 'Import Success',
|
||||||
|
importSuccessDesc: 'Workflow imported successfully, you can view and manage it in the application management',
|
||||||
|
gotoList: 'Return to Application List',
|
||||||
|
gotoDetail: 'View Details',
|
||||||
|
dify: 'Dify',
|
||||||
},
|
},
|
||||||
userMemory: {
|
userMemory: {
|
||||||
userMemory: 'User Memory',
|
userMemory: 'User Memory',
|
||||||
|
|||||||
@@ -736,12 +736,21 @@ export const zh = {
|
|||||||
dynamicMatchSkill: '动态匹配技能',
|
dynamicMatchSkill: '动态匹配技能',
|
||||||
executeTask: '执行任务',
|
executeTask: '执行任务',
|
||||||
|
|
||||||
|
importWorkflow: '导入工作流',
|
||||||
|
platform: '来源平台',
|
||||||
upload: '上传与解析',
|
upload: '上传与解析',
|
||||||
complex: '兼容性分析',
|
complex: '兼容性分析',
|
||||||
node: '节点映射',
|
|
||||||
configCheck: '配置校验',
|
|
||||||
sureInfo: '信息确认',
|
sureInfo: '信息确认',
|
||||||
completed: '完成导入',
|
completed: '完成导入',
|
||||||
|
baseInfo: '基本信息',
|
||||||
|
workflowName: '工作流名称',
|
||||||
|
fileName: '文件名称',
|
||||||
|
fileSize: '文件大小',
|
||||||
|
importSuccess: '导入成功',
|
||||||
|
importSuccessDesc: '您的工作流已成功导入,可以在应用管理中查看和管理',
|
||||||
|
gotoList: '返回应用列表',
|
||||||
|
gotoDetail: '查看详情',
|
||||||
|
dify: 'Dify',
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
totalRecords: '共 {{total}} 条记录'
|
totalRecords: '共 {{total}} 条记录'
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:27:52
|
* @Date: 2026-02-03 16:27:52
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-03 16:27:52
|
* @Last Modified time: 2026-02-28 16:48:52
|
||||||
*/
|
*/
|
||||||
import { type FC, useRef } from 'react';
|
import { type FC, useRef, useMemo } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Layout, Tabs, Dropdown, Button, Flex } from 'antd';
|
import { Layout, Tabs, Dropdown, Button, Flex } from 'antd';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
@@ -21,6 +21,7 @@ import ApplicationModal from '@/views/ApplicationManagement/components/Applicati
|
|||||||
import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef } from '../types'
|
import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef } from '../types'
|
||||||
import { deleteApplication } from '@/api/application'
|
import { deleteApplication } from '@/api/application'
|
||||||
import CopyModal from './CopyModal'
|
import CopyModal from './CopyModal'
|
||||||
|
import { exportToYaml } from '@/utils/yamlExport';
|
||||||
|
|
||||||
const { Header } = Layout;
|
const { Header } = Layout;
|
||||||
|
|
||||||
@@ -80,20 +81,6 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
label: t(`application.${key}`),
|
label: t(`application.${key}`),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Format dropdown menu items
|
|
||||||
*/
|
|
||||||
const formatMenuItems = () => {
|
|
||||||
const items = ['edit', 'copy', 'export', 'delete'].map(key => ({
|
|
||||||
key,
|
|
||||||
icon: <img src={menuIcons[key]} className="rb:w-4 rb:h-4 rb:mr-2" />,
|
|
||||||
label: t(`common.${key}`),
|
|
||||||
}))
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
onClick: handleClick
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Handle menu item click
|
* Handle menu item click
|
||||||
*/
|
*/
|
||||||
@@ -106,6 +93,8 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
copyModalRef.current?.handleOpen()
|
copyModalRef.current?.handleOpen()
|
||||||
break;
|
break;
|
||||||
case 'export':
|
case 'export':
|
||||||
|
console.log('export', workflowRef?.current?.config)
|
||||||
|
exportToYaml(workflowRef?.current?.config, application?.name ?`${application?.name}.yml`: undefined)
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
handleDelete()
|
handleDelete()
|
||||||
@@ -160,6 +149,19 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
const addvariable = () => {
|
const addvariable = () => {
|
||||||
workflowRef?.current?.addVariable()
|
workflowRef?.current?.addVariable()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Format dropdown menu items
|
||||||
|
*/
|
||||||
|
const formatMenuItems = useMemo(() => {
|
||||||
|
const items = (application?.type === 'workflow' ? ['edit', 'copy', 'export', 'delete'] : ['edit', 'copy', 'delete']).map(key => ({
|
||||||
|
key,
|
||||||
|
icon: <img src={menuIcons[key]} className="rb:w-4 rb:h-4 rb:mr-2" />,
|
||||||
|
label: t(`common.${key}`),
|
||||||
|
}))
|
||||||
|
return items
|
||||||
|
}, [t, handleClick, application])
|
||||||
|
|
||||||
|
console.log('formatMenuItems', formatMenuItems)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
|
<Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
|
||||||
@@ -170,7 +172,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
|
|
||||||
<div className="rb:max-w-[100%-80px] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{application?.name}</div>
|
<div className="rb:max-w-[100%-80px] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{application?.name}</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={formatMenuItems()}
|
menu={{ items: formatMenuItems, onClick: handleClick }}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:29:49
|
* @Date: 2026-02-03 16:29:49
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-05 10:31:10
|
* @Last Modified time: 2026-02-28 16:40:30
|
||||||
*/
|
*/
|
||||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||||
import type { Variable } from './components/VariableList/types'
|
import type { Variable } from './components/VariableList/types'
|
||||||
import type { ToolOption } from './components/ToolList/types'
|
import type { ToolOption } from './components/ToolList/types'
|
||||||
import type { ChatItem } from '@/components/Chat/types'
|
import type { ChatItem } from '@/components/Chat/types'
|
||||||
import type { GraphRef } from '@/views/Workflow/types';
|
import type { GraphRef, WorkflowConfig } from '@/views/Workflow/types';
|
||||||
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
||||||
import type { SkillConfigForm } from './components/Skill/types'
|
import type { SkillConfigForm } from './components/Skill/types'
|
||||||
|
|
||||||
@@ -155,6 +155,7 @@ export interface WorkflowRef {
|
|||||||
graphRef: GraphRef;
|
graphRef: GraphRef;
|
||||||
/** Add variable */
|
/** Add variable */
|
||||||
addVariable: () => void;
|
addVariable: () => void;
|
||||||
|
config: WorkflowConfig | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,98 +1,203 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-02-28 14:08:14
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-02-28 16:20:40
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* UploadWorkflowModal Component
|
||||||
|
*
|
||||||
|
* This component provides a modal for uploading workflow files with a multi-step process:
|
||||||
|
* 1. Upload - Select platform and file
|
||||||
|
* 2. Complex - Show warnings and errors if any
|
||||||
|
* 3. SureInfo - Confirm and edit workflow information
|
||||||
|
* 4. Completed - Show success message and options
|
||||||
|
*/
|
||||||
import { forwardRef, useImperativeHandle, useState, useMemo } from 'react';
|
import { forwardRef, useImperativeHandle, useState, useMemo } from 'react';
|
||||||
import { Form, Select, Steps, Flex, Alert, Row, Col, Statistic, Input, Button } from 'antd';
|
import { Form, Select, Steps, Flex, Alert, Input, Button, Result } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type { UploadWorkflowModalData, UploadWorkflowModalRef } from '../types'
|
import type { UploadWorkflowModalData, UploadData, UploadWorkflowModalRef } from '../types'
|
||||||
import RbModal from '@/components/RbModal'
|
import RbModal from '@/components/RbModal'
|
||||||
import UploadFiles from '@/components/Upload/UploadFiles'
|
import UploadFiles from '@/components/Upload/UploadFiles'
|
||||||
import { fileUploadUrl } from '@/api/fileStorage'
|
import { importWorkflow, completeImportWorkflow } from '@/api/application'
|
||||||
import RbCard from '@/components/RbCard/Card'
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for UploadWorkflowModal component
|
||||||
|
*/
|
||||||
interface UploadWorkflowModalProps {
|
interface UploadWorkflowModalProps {
|
||||||
|
/** Function to refresh the parent component after workflow import */
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Steps definition for the upload process
|
||||||
|
*/
|
||||||
const steps = [
|
const steps = [
|
||||||
'upload',
|
'upload', // Step 1: File upload
|
||||||
'complex',
|
'complex', // Step 2: Error/warning display
|
||||||
'node',
|
'sureInfo', // Step 3: Information confirmation
|
||||||
'configCheck',
|
'completed' // Step 4: Success message
|
||||||
'sureInfo',
|
|
||||||
'completed'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UploadWorkflowModal component
|
||||||
|
*
|
||||||
|
* @param {UploadWorkflowModalProps} props - Component props
|
||||||
|
* @param {React.Ref<UploadWorkflowModalRef>} ref - Ref for imperative methods
|
||||||
|
*/
|
||||||
const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowModalProps>(({
|
const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowModalProps>(({
|
||||||
refresh
|
refresh
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [form] = Form.useForm<UploadWorkflowModalData>();
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [current, setCurrent] = useState<number>(5);
|
|
||||||
|
|
||||||
// 封装取消方法,添加关闭弹窗逻辑
|
// State management
|
||||||
|
const [visible, setVisible] = useState(false); // Modal visibility
|
||||||
|
const [form] = Form.useForm<UploadWorkflowModalData>(); // Form instance
|
||||||
|
const [loading, setLoading] = useState(false); // Loading state
|
||||||
|
const [current, setCurrent] = useState<number>(0); // Current step
|
||||||
|
const [data, setData] = useState<UploadData | null>(null); // Upload response data
|
||||||
|
const [firstFormData, setFirstFormData] = useState<UploadWorkflowModalData | null>(null); // First step form data
|
||||||
|
const [appId, setAppId] = useState<string | null>(null); // Imported application ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle modal close
|
||||||
|
* Resets all states and form fields
|
||||||
|
*/
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
setLoading(false)
|
setData(null);
|
||||||
|
setCurrent(0);
|
||||||
|
setFirstFormData(null);
|
||||||
|
setAppId(null);
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle modal open
|
||||||
|
* Resets form fields and shows modal
|
||||||
|
*/
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
};
|
};
|
||||||
// 封装保存方法,添加提交逻辑
|
|
||||||
|
/**
|
||||||
|
* Handle save/submit action
|
||||||
|
* Processes different logic based on current step
|
||||||
|
*/
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
const values = form.getFieldsValue();
|
||||||
|
|
||||||
switch(current) {
|
switch(current) {
|
||||||
case 0:
|
case 0: // Step 1: Upload file
|
||||||
setCurrent(1)
|
const formData = new FormData();
|
||||||
|
setFirstFormData(values);
|
||||||
|
formData.append('platform', values.platform);
|
||||||
|
formData.append('file', values.file[0]);
|
||||||
|
|
||||||
|
// Call import workflow API
|
||||||
|
importWorkflow(formData)
|
||||||
|
.then(res => {
|
||||||
|
const response = res as UploadData;
|
||||||
|
const { errors, warnings } = response;
|
||||||
|
setData(response);
|
||||||
|
|
||||||
|
// Navigate to error/warning step if any, otherwise go to confirmation
|
||||||
|
if (errors.length || warnings.length) {
|
||||||
|
setCurrent(1);
|
||||||
|
} else {
|
||||||
|
setCurrent(2);
|
||||||
|
// Pre-fill form with file information
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: values.file[0].name.split('.')[0],
|
||||||
|
platform: values.platform,
|
||||||
|
fileName: values.file[0].name,
|
||||||
|
fileSize: values.file[0].size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1: // Step 2: Error/warning display
|
||||||
setCurrent(2)
|
if (firstFormData) {
|
||||||
|
const { file, platform } = firstFormData;
|
||||||
|
// Pre-fill form with file information
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: file[0].name.split('.')[0],
|
||||||
|
platform: platform,
|
||||||
|
fileName: file[0].name,
|
||||||
|
fileSize: file[0].size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setCurrent(2);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2: // Step 3: Confirm information
|
||||||
setCurrent(3)
|
if (data) {
|
||||||
break;
|
// Complete import workflow
|
||||||
case 3:
|
completeImportWorkflow({
|
||||||
setCurrent(4)
|
temp_id: data.temp_id,
|
||||||
break;
|
name: values.name,
|
||||||
case 4:
|
description: values.description,
|
||||||
setCurrent(5)
|
})
|
||||||
break;
|
.then((res) => {
|
||||||
case 5:
|
const response = res as { id: string };
|
||||||
|
setCurrent(3);
|
||||||
|
setAppId(response.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
setCurrent(prev => prev + 1)
|
setCurrent(prev => prev + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// form
|
};
|
||||||
// .validateFields()
|
|
||||||
// .then(() => {
|
|
||||||
// })
|
|
||||||
// .catch((err) => {
|
|
||||||
// console.log('err', err)
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 暴露给父组件的方法
|
// Expose methods to parent component via ref
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleOpen,
|
handleOpen,
|
||||||
handleClose
|
handleClose
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle navigation to previous step
|
||||||
|
* Adjusts step based on whether there were errors/warnings
|
||||||
|
*/
|
||||||
const handleLastStep = () => {
|
const handleLastStep = () => {
|
||||||
setCurrent(prev => prev - 1)
|
let newStep = current - 1;
|
||||||
}
|
// If no errors or warnings, skip the error/warning step
|
||||||
|
if (!data?.warnings?.length && !data?.errors?.length) {
|
||||||
|
newStep = current - 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form if not going back to error/warning step
|
||||||
|
if (newStep !== 1) {
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
setCurrent(newStep);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle navigation after successful import
|
||||||
|
* @param {string} type - Navigation type ('detail' or 'list')
|
||||||
|
*/
|
||||||
const handleJump = (type: string) => {
|
const handleJump = (type: string) => {
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'detail':
|
case 'detail':
|
||||||
break;
|
// Open application detail page in new tab
|
||||||
default:
|
window.open(`/#/application/config/${appId}`, '_blank');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
refresh();
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate modal footer based on current step
|
||||||
|
*/
|
||||||
const getFooter = useMemo(() => {
|
const getFooter = useMemo(() => {
|
||||||
switch(current) {
|
switch(current) {
|
||||||
case 0:
|
case 0: // Step 1: Upload
|
||||||
return [
|
return [
|
||||||
<Button key="back" onClick={handleClose}>
|
<Button key="back" onClick={handleClose}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
@@ -103,30 +208,18 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
>
|
>
|
||||||
{t('application.nextStep')}
|
{t('common.nextStep')}
|
||||||
</Button>
|
</Button>
|
||||||
]
|
];
|
||||||
case 5:
|
case 3: // Step 4: Completed
|
||||||
return [
|
return null;
|
||||||
<Button key="back" onClick={() => handleJump('list')}>
|
default: // Steps 1-2
|
||||||
{t('application.gotoList')}
|
|
||||||
</Button>,
|
|
||||||
<Button
|
|
||||||
key="submit"
|
|
||||||
type="primary"
|
|
||||||
loading={loading}
|
|
||||||
onClick={() => handleJump('detail')}
|
|
||||||
>
|
|
||||||
{t('application.gotoDetail')}
|
|
||||||
</Button>
|
|
||||||
]
|
|
||||||
default:
|
|
||||||
return [
|
return [
|
||||||
<Button onClick={handleClose}>
|
<Button onClick={handleClose}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button key="back" onClick={handleLastStep}>
|
<Button key="back" onClick={handleLastStep}>
|
||||||
{t('application.lastStep')}
|
{t('common.prevStep')}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
key="submit"
|
key="submit"
|
||||||
@@ -134,11 +227,11 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
>
|
>
|
||||||
{t('application.nextStep')}
|
{t('common.nextStep')}
|
||||||
</Button>
|
</Button>
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
}, [current])
|
}, [current]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RbModal
|
<RbModal
|
||||||
@@ -150,6 +243,7 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
|||||||
footer={getFooter}
|
footer={getFooter}
|
||||||
width={1000}
|
width={1000}
|
||||||
>
|
>
|
||||||
|
{/* Steps indicator */}
|
||||||
<div className='rb:p-3 rb:bg-[#FBFDFF] rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:mb-3'>
|
<div className='rb:p-3 rb:bg-[#FBFDFF] rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:mb-3'>
|
||||||
<Steps
|
<Steps
|
||||||
labelPlacement="vertical"
|
labelPlacement="vertical"
|
||||||
@@ -158,25 +252,29 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
|||||||
items={steps.map(key => ({ title: t(`application.${key}`) }))}
|
items={steps.map(key => ({ title: t(`application.${key}`) }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Step 1: File upload */}
|
||||||
{current === 0 &&
|
{current === 0 &&
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
|
initialValues={{
|
||||||
|
platform: 'dify'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item name="provider" label={t('application.workflowProvider')}>
|
<Form.Item name="platform" label={t('application.platform')}>
|
||||||
<Select
|
<Select
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
options={[
|
options={['dify'].map(value => ({
|
||||||
{ label: 'Dify', value: 'dify' },
|
label: t(`application.${value}`), value: value,
|
||||||
]}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="file" valuePropName="fileList" noStyle>
|
<Form.Item name="file" valuePropName="fileList" noStyle>
|
||||||
<UploadFiles
|
<UploadFiles
|
||||||
action={fileUploadUrl}
|
isAutoUpload={false}
|
||||||
isCanDrag={true}
|
isCanDrag={true}
|
||||||
fileSize={100}
|
fileSize={100}
|
||||||
multiple={true}
|
|
||||||
maxCount={1}
|
maxCount={1}
|
||||||
fileType={['yml', 'yaml', 'zip', 'json']}
|
fileType={['yml', 'yaml', 'zip', 'json']}
|
||||||
onChange={(fileList) => {
|
onChange={(fileList) => {
|
||||||
@@ -187,78 +285,76 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
|||||||
</Form>
|
</Form>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{/* Step 2: Error/warning display */}
|
||||||
{current === 1 &&
|
{current === 1 &&
|
||||||
<Flex vertical gap={12} className="rb:w-[70%]! rb:mx-auto!">
|
<Flex vertical gap={12} className="rb:w-[70%]! rb:mx-auto!">
|
||||||
{['fileType', 'parse', 'nodes', 'variable'].map(key => (
|
{data?.warnings.map(vo => (
|
||||||
<Alert key={key} message={key} type="success" showIcon />
|
<Alert
|
||||||
|
key={vo.node_id}
|
||||||
|
message={<div>
|
||||||
|
<div>{vo.node_name || vo.node_id} - {vo.type}</div>
|
||||||
|
{vo.detail}
|
||||||
|
</div>}
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{data?.errors.map(vo => (
|
||||||
|
<Alert
|
||||||
|
key={vo.node_id}
|
||||||
|
message={<div>
|
||||||
|
<div>{vo.node_name || vo.node_id} - {vo.type}</div>
|
||||||
|
{vo.detail}
|
||||||
|
</div>}
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Row gutter={12}>
|
|
||||||
{['complex', 'nodes', 'task'].map(key => (
|
|
||||||
<Col key={key} span={8}>
|
|
||||||
<Statistic title={key} value={0} className="rb:text-center rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3!" />
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
|
|
||||||
{/* 节点映射 */}
|
{/* Step 3: Information confirmation */}
|
||||||
{current === 2 &&
|
{current === 2 &&
|
||||||
<Flex vertical gap={12} className="rb:w-[70%]! rb:mx-auto!">
|
<Form form={form} layout="vertical" className="rb:w-[70%]! rb:mx-auto!">
|
||||||
<RbCard>
|
|
||||||
<Flex justify="space-around">
|
|
||||||
<div> Left Node</div>
|
|
||||||
→
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
placeholder={t('common.pleaseSelect')}
|
|
||||||
className="rb:w-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
</RbCard>
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
{current === 3 &&
|
|
||||||
<Flex vertical gap={12} className="rb:w-[70%]! rb:mx-auto!">
|
|
||||||
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
{current === 4 &&
|
|
||||||
<Form form={form} layout="horizontal" className="rb:w-[70%]! rb:mx-auto!">
|
|
||||||
<div className="rb:text-[#5B6167] rb:font-medium">{t('application.baseInfo')}</div>
|
<div className="rb:text-[#5B6167] rb:font-medium">{t('application.baseInfo')}</div>
|
||||||
<Form.Item name="name" label={t('application.workflowName')} rules={[{ required: true }]}>
|
<Form.Item name="name" label={t('application.workflowName')} rules={[{ required: true }]}>
|
||||||
<Input placeholder={t('common.pleaseEnter')} />
|
<Input placeholder={t('common.pleaseEnter')} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="source" label={t('application.source')}>
|
<Form.Item name="platform" label={t('application.platform')}>
|
||||||
source
|
<Input disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="fileName" label={t('application.fileName')}>
|
<Form.Item name="fileName" label={t('application.fileName')}>
|
||||||
fileName
|
<Input disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="fileSize" label={t('application.fileSize')}>
|
<Form.Item name="fileSize" label={t('application.fileSize')}>
|
||||||
fileSize
|
<Input disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="desciption" label={t('application.desciption')}>
|
<Form.Item name="description" label={t('application.description')} layout="vertical">
|
||||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<div className="rb:text-[#5B6167] rb:font-medium">{t('application.importStatistic')}</div>
|
|
||||||
<Row gutter={12}>
|
|
||||||
{['complex', 'nodes', 'task'].map(key => (
|
|
||||||
<Col key={key} span={8}>
|
|
||||||
<Statistic title={key} value={0} className="rb:text-center rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3!" />
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
</Form>
|
</Form>
|
||||||
}
|
}
|
||||||
{current === 5 &&
|
|
||||||
<Flex justify="center" vertical gap={12} className="rb:w-[70%]! rb:mx-auto! rb:text-center">
|
{/* Step 4: Success message */}
|
||||||
<div>导入成功</div>
|
{current === 3 &&
|
||||||
<div>您的工作流已成功导入,可以在应用管理中查看和管理</div>
|
<Result
|
||||||
</Flex>
|
status="success"
|
||||||
|
title={t('application.importSuccess')}
|
||||||
|
subTitle={t('application.importSuccessDesc')}
|
||||||
|
extra={[
|
||||||
|
<Button key="back" onClick={() => handleJump('list')}>
|
||||||
|
{t('application.gotoList')}
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={() => handleJump('detail')}
|
||||||
|
>
|
||||||
|
{t('application.gotoDetail')}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</RbModal>
|
</RbModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,9 +83,9 @@ const ApplicationManagement: React.FC = () => {
|
|||||||
setQuery(prev => ({...prev, type: value}))
|
setQuery(prev => ({...prev, type: value}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// const handleImport = () => {
|
const handleImport = () => {
|
||||||
// uploadWorkflowModalRef.current?.handleOpen()
|
uploadWorkflowModalRef.current?.handleOpen()
|
||||||
// }
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row gutter={16} className="rb:mb-4">
|
<Row gutter={16} className="rb:mb-4">
|
||||||
@@ -111,9 +111,9 @@ const ApplicationManagement: React.FC = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={12} className="rb:text-right">
|
<Col span={12} className="rb:text-right">
|
||||||
<Space size={12}>
|
<Space size={12}>
|
||||||
{/* <Button onClick={handleImport}>
|
<Button onClick={handleImport}>
|
||||||
{t('application.importWorkflow')}
|
{t('application.importWorkflow')}
|
||||||
</Button> */}
|
</Button>
|
||||||
<Button type="primary" onClick={handleCreate}>
|
<Button type="primary" onClick={handleCreate}>
|
||||||
{t('application.createApplication')}
|
{t('application.createApplication')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:34:15
|
* @Date: 2026-02-03 16:34:15
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-06 11:08:37
|
* @Last Modified time: 2026-02-28 16:16:03
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Type definitions for Application Management
|
* Type definitions for Application Management
|
||||||
*/
|
*/
|
||||||
|
import type { WorkflowConfig } from '@/views/Workflow/types';
|
||||||
/**
|
/**
|
||||||
* Search query parameters
|
* Search query parameters
|
||||||
*/
|
*/
|
||||||
@@ -174,9 +174,63 @@ export interface ApiExtensionModalRef {
|
|||||||
handleOpen: () => void;
|
handleOpen: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload workflow modal form data
|
||||||
|
*/
|
||||||
export interface UploadWorkflowModalData {
|
export interface UploadWorkflowModalData {
|
||||||
|
/** Platform type (e.g., 'dify') */
|
||||||
|
platform: string;
|
||||||
|
/** Array of uploaded files */
|
||||||
|
file: any[];
|
||||||
|
/** Optional workflow name */
|
||||||
|
name?: string;
|
||||||
|
/** Optional original file name */
|
||||||
|
fileName?: string;
|
||||||
|
/** Optional file size in bytes */
|
||||||
|
fileSize?: number;
|
||||||
|
/** Optional workflow description */
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complex item for errors and warnings
|
||||||
|
*/
|
||||||
|
interface ComplexItem {
|
||||||
|
/** Error/warning type */
|
||||||
|
type: string;
|
||||||
|
/** Detailed error/warning message */
|
||||||
|
detail: string;
|
||||||
|
/** Node identifier where the error/warning occurred */
|
||||||
|
node_id: string;
|
||||||
|
/** Node name where the error/warning occurred */
|
||||||
|
node_name: string;
|
||||||
|
/** Optional scope of the error/warning */
|
||||||
|
scope: string | null;
|
||||||
|
/** Optional name associated with the error/warning */
|
||||||
|
name: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload data response
|
||||||
|
* @extends WorkflowConfig
|
||||||
|
*/
|
||||||
|
export interface UploadData extends WorkflowConfig {
|
||||||
|
/** Whether the upload was successful */
|
||||||
|
success: boolean;
|
||||||
|
/** Temporary identifier for the uploaded workflow */
|
||||||
|
temp_id: string;
|
||||||
|
/** Optional workflow identifier if already exists */
|
||||||
|
workflow_id?: string;
|
||||||
|
/** Array of error items */
|
||||||
|
errors: ComplexItem[];
|
||||||
|
/** Array of warning items */
|
||||||
|
warnings: ComplexItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload workflow modal ref interface
|
||||||
|
*/
|
||||||
export interface UploadWorkflowModalRef {
|
export interface UploadWorkflowModalRef {
|
||||||
|
/** Open the upload workflow modal */
|
||||||
handleOpen: () => void;
|
handleOpen: () => void;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2025-12-30 13:59:36
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-02-28 16:19:26
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* ChatVariableModal Component
|
||||||
|
*
|
||||||
|
* This component provides a modal for adding or editing chat variables in workflows.
|
||||||
|
* It supports various variable types and provides appropriate input fields based on the selected type.
|
||||||
|
*/
|
||||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||||
import { Form, Input, Select, InputNumber } from 'antd';
|
import { Form, Input, Select, InputNumber } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -8,54 +20,86 @@ import RbModal from '@/components/RbModal'
|
|||||||
|
|
||||||
const FormItem = Form.Item;
|
const FormItem = Form.Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for ChatVariableModal component
|
||||||
|
*/
|
||||||
interface ChatVariableModalProps {
|
interface ChatVariableModalProps {
|
||||||
|
/**
|
||||||
|
* Callback function to refresh variable list
|
||||||
|
* @param {ChatVariable} value - The variable data
|
||||||
|
* @param {number} [editIndex] - Optional index for editing existing variable
|
||||||
|
*/
|
||||||
refresh: (value: ChatVariable, editIndex?: number) => void;
|
refresh: (value: ChatVariable, editIndex?: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported variable types
|
||||||
|
*/
|
||||||
const types = [
|
const types = [
|
||||||
'string',
|
'string', // String type
|
||||||
'number',
|
'number', // Number type
|
||||||
'boolean',
|
'boolean', // Boolean type
|
||||||
|
'object', // Object type
|
||||||
|
'array[string]', // Array of strings
|
||||||
|
'array[number]', // Array of numbers
|
||||||
|
'array[boolean]', // Array of booleans
|
||||||
|
'array[object]', // Array of objects
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatVariableModal component
|
||||||
|
*/
|
||||||
const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProps>(({
|
const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProps>(({
|
||||||
refresh
|
refresh
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [form] = Form.useForm<ChatVariable>();
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined)
|
|
||||||
const type = Form.useWatch('type', form);
|
|
||||||
|
|
||||||
// 封装取消方法,添加关闭弹窗逻辑
|
// State management
|
||||||
|
const [visible, setVisible] = useState(false); // Modal visibility
|
||||||
|
const [form] = Form.useForm<ChatVariable>(); // Form instance
|
||||||
|
const [loading, setLoading] = useState(false); // Loading state
|
||||||
|
const [editIndex, setEditIndex] = useState<number | undefined>(undefined); // Index of variable being edited
|
||||||
|
const type = Form.useWatch('type', form); // Current selected type
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle modal close
|
||||||
|
*/
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
setEditIndex(undefined)
|
setEditIndex(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle modal open
|
||||||
|
*/
|
||||||
const handleOpen = (variable?: ChatVariable, index?: number) => {
|
const handleOpen = (variable?: ChatVariable, index?: number) => {
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
if (variable) {
|
if (variable) {
|
||||||
const { default: _, ...rest } = variable
|
// Exclude 'default' property and set form values
|
||||||
form.setFieldsValue({ ...rest })
|
const { default: _, ...rest } = variable;
|
||||||
setEditIndex(index)
|
form.setFieldsValue({ ...rest });
|
||||||
|
setEditIndex(index);
|
||||||
} else {
|
} else {
|
||||||
|
// Reset form for new variable
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
setEditIndex(undefined)
|
setEditIndex(undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 封装保存方法,添加提交逻辑
|
|
||||||
|
/**
|
||||||
|
* Handle save/submit action
|
||||||
|
*/
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
form.validateFields().then((values) => {
|
form.validateFields().then((values) => {
|
||||||
refresh({ ...values, default: values.defaultValue }, editIndex)
|
// Create variable with 'default' property mapped from 'defaultValue'
|
||||||
handleClose()
|
refresh({ ...values, default: values.defaultValue }, editIndex);
|
||||||
})
|
handleClose();
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 暴露给父组件的方法
|
// Expose handleOpen method to parent component via ref
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleOpen
|
handleOpen
|
||||||
}));
|
}));
|
||||||
@@ -74,6 +118,7 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||||
>
|
>
|
||||||
|
{/* Variable name field */}
|
||||||
<FormItem
|
<FormItem
|
||||||
name="name"
|
name="name"
|
||||||
label={t('workflow.config.parameter-extractor.name')}
|
label={t('workflow.config.parameter-extractor.name')}
|
||||||
@@ -84,6 +129,8 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
|||||||
>
|
>
|
||||||
<Input placeholder={t('common.enter')} />
|
<Input placeholder={t('common.enter')} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
|
{/* Variable type field */}
|
||||||
<FormItem
|
<FormItem
|
||||||
name="type"
|
name="type"
|
||||||
label={t('workflow.config.parameter-extractor.type')}
|
label={t('workflow.config.parameter-extractor.type')}
|
||||||
@@ -98,6 +145,8 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
|
{/* Default value field - dynamic based on type */}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="defaultValue"
|
name="defaultValue"
|
||||||
label={t('workflow.config.parameter-extractor.default')}
|
label={t('workflow.config.parameter-extractor.default')}
|
||||||
@@ -119,6 +168,8 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
|||||||
: <Input placeholder={t('common.enter')} />
|
: <Input placeholder={t('common.enter')} />
|
||||||
}
|
}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Variable description field */}
|
||||||
<FormItem
|
<FormItem
|
||||||
name="description"
|
name="description"
|
||||||
label={t('workflow.config.parameter-extractor.desc')}
|
label={t('workflow.config.parameter-extractor.desc')}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-06 21:10:56
|
* @Date: 2026-02-06 21:10:56
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-27 09:58:30
|
* @Last Modified time: 2026-02-28 16:43:06
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Workflow Chat Component
|
* Workflow Chat Component
|
||||||
@@ -97,6 +97,8 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
|||||||
setConversationId(null)
|
setConversationId(null)
|
||||||
setMessage(undefined)
|
setMessage(undefined)
|
||||||
setFileList([])
|
setFileList([])
|
||||||
|
setLoading(false)
|
||||||
|
setStreamLoading(false)
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Opens the variable configuration modal
|
* Opens the variable configuration modal
|
||||||
@@ -179,6 +181,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
|||||||
cycle_idx: number;
|
cycle_idx: number;
|
||||||
node_id: string;
|
node_id: string;
|
||||||
node_name?: string;
|
node_name?: string;
|
||||||
|
node_type?: string;
|
||||||
input?: any;
|
input?: any;
|
||||||
output?: any;
|
output?: any;
|
||||||
elapsed_time?: string;
|
elapsed_time?: string;
|
||||||
@@ -188,7 +191,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
|||||||
};
|
};
|
||||||
|
|
||||||
const node = graphRef.current?.getNodes().find(n => n.id === node_id);
|
const node = graphRef.current?.getNodes().find(n => n.id === node_id);
|
||||||
const { name, icon } = node?.getData() || {}
|
const { name, icon, type } = node?.getData() || {}
|
||||||
|
|
||||||
switch(item.event) {
|
switch(item.event) {
|
||||||
// Append streaming text chunks to assistant message
|
// Append streaming text chunks to assistant message
|
||||||
@@ -218,6 +221,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
|||||||
...newSubContent[filterIndex],
|
...newSubContent[filterIndex],
|
||||||
node_id: node_id,
|
node_id: node_id,
|
||||||
node_name: name,
|
node_name: name,
|
||||||
|
node_type: type,
|
||||||
icon,
|
icon,
|
||||||
content: {},
|
content: {},
|
||||||
}
|
}
|
||||||
@@ -226,6 +230,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
|||||||
id: node_id,
|
id: node_id,
|
||||||
node_id: node_id,
|
node_id: node_id,
|
||||||
node_name: name,
|
node_name: name,
|
||||||
|
node_type: type,
|
||||||
icon,
|
icon,
|
||||||
content: {},
|
content: {},
|
||||||
})
|
})
|
||||||
@@ -282,6 +287,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
|||||||
cycle_idx,
|
cycle_idx,
|
||||||
node_id,
|
node_id,
|
||||||
node_name: name,
|
node_name: name,
|
||||||
|
node_type: type,
|
||||||
icon,
|
icon,
|
||||||
content: {
|
content: {
|
||||||
cycle_idx,
|
cycle_idx,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-24 17:57:08
|
* @Date: 2026-02-24 17:57:08
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-24 17:57:08
|
* @Last Modified time: 2026-02-28 16:48:09
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
* Runtime Component
|
* Runtime Component
|
||||||
@@ -105,7 +105,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
if (Array.isArray(list)) {
|
if (Array.isArray(list)) {
|
||||||
return <Space size={8} direction="vertical" className="rb:w-full!">
|
return <Space size={8} direction="vertical" className="rb:w-full!">
|
||||||
{list?.map(vo => {
|
{list?.map(vo => {
|
||||||
const isLoop = vo.node_id.startsWith('loop');
|
const isLoop = vo.node_type === 'loop';
|
||||||
// Render cycle variables for loop nodes without node_name
|
// Render cycle variables for loop nodes without node_name
|
||||||
if (typeof vo.cycle_idx === 'number' && isLoop && !vo.node_name) {
|
if (typeof vo.cycle_idx === 'number' && isLoop && !vo.node_name) {
|
||||||
return <div className="rb:bg-[#F0F3F8] rb:rounded-md">
|
return <div className="rb:bg-[#F0F3F8] rb:rounded-md">
|
||||||
@@ -165,7 +165,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
}
|
}
|
||||||
{/* Display navigation to nested cycles if subContent exists */}
|
{/* Display navigation to nested cycles if subContent exists */}
|
||||||
{vo.subContent?.length > 0 && (
|
{vo.subContent?.length > 0 && (
|
||||||
<Flex justify="space-between" className="rb:bg-[#F0F3F8] rb:rounded-md rb:py-2! rb:px-3! rb:cursor-pointer" onClick={() => handleViewDetail(vo, vo.node_id.startsWith('loop'))}>
|
<Flex justify="space-between" className="rb:bg-[#F0F3F8] rb:rounded-md rb:py-2! rb:px-3! rb:cursor-pointer" onClick={() => handleViewDetail(vo, vo.node_type === 'loop')}>
|
||||||
<span>{Math.max(...vo.subContent.map((itemVo: any) => itemVo.cycle_idx + 1))} {t(`workflow.${isLoop ? 'loopNum' : 'iterationNum'}`)}</span>
|
<span>{Math.max(...vo.subContent.map((itemVo: any) => itemVo.cycle_idx + 1))} {t(`workflow.${isLoop ? 'loopNum' : 'iterationNum'}`)}</span>
|
||||||
<RightOutlined />
|
<RightOutlined />
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -217,7 +217,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
children: (
|
children: (
|
||||||
detail
|
detail
|
||||||
? (
|
? (
|
||||||
<div className="rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 rb:mt-2">
|
<div className="rb:bg-[#FBFDFF] rb:rounded-md">
|
||||||
<Button type="link" icon={<ArrowLeftOutlined />} onClick={() => setDetail(null)} className="rb:px-0! rb:text-[12px]!">
|
<Button type="link" icon={<ArrowLeftOutlined />} onClick={() => setDetail(null)} className="rb:px-0! rb:text-[12px]!">
|
||||||
{t('common.return')}
|
{t('common.return')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -28,9 +28,6 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
:global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
.collapse-item :global(.ant-collapse) {
|
.collapse-item :global(.ant-collapse) {
|
||||||
/* background-color: #F0F3F8; */
|
/* background-color: #F0F3F8; */
|
||||||
background-color: #FBFDFF;
|
background-color: #FBFDFF;
|
||||||
@@ -41,5 +38,5 @@
|
|||||||
border-radius: 0 0 6px 6px;
|
border-radius: 0 0 6px 6px;
|
||||||
}
|
}
|
||||||
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||||
padding: 0 4px 4px 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-09 18:24:53
|
* @Date: 2026-02-09 18:24:53
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-09 18:24:53
|
* @Last Modified time: 2026-02-28 17:49:28
|
||||||
*/
|
*/
|
||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@@ -292,7 +292,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||||
const leftFieldType = leftFieldOption?.dataType;
|
const leftFieldType = leftFieldOption?.dataType;
|
||||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
const inputType = leftFieldType === 'number' ? currentExpression.input_type?.toLocaleLowerCase() : undefined;
|
||||||
return (
|
return (
|
||||||
<div key={conditionField.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
<div key={conditionField.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||||
@@ -330,7 +330,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||||
<Select
|
<Select
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
options={[{ value: 'variable', label: 'Variable' }, { value: 'constant', label: 'Constant' }]}
|
||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
||||||
@@ -339,7 +339,7 @@ const CaseList: FC<CaseListProps> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||||
{inputType === 'Variable'
|
{inputType === 'variable'
|
||||||
?
|
?
|
||||||
<VariableSelect
|
<VariableSelect
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ const ConditionList: FC<CaseListProps> = ({
|
|||||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||||
const leftFieldType = leftFieldOption?.dataType;
|
const leftFieldType = leftFieldOption?.dataType;
|
||||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
const inputType = leftFieldType === 'number' ? currentExpression.input_type?.toLocaleLowerCase() : undefined;
|
||||||
|
console.log('inputType', inputType)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={field.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
<div key={field.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||||
@@ -160,7 +161,7 @@ const ConditionList: FC<CaseListProps> = ({
|
|||||||
<Form.Item name={[field.name, 'input_type']} noStyle>
|
<Form.Item name={[field.name, 'input_type']} noStyle>
|
||||||
<Select
|
<Select
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
options={[{ value: 'variable', label: 'Variable' }, { value: 'constant', label: 'Constant' }]}
|
||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
className="rb:w-full!"
|
className="rb:w-full!"
|
||||||
@@ -169,7 +170,7 @@ const ConditionList: FC<CaseListProps> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Form.Item name={[field.name, 'right']} noStyle>
|
<Form.Item name={[field.name, 'right']} noStyle>
|
||||||
{inputType === 'Variable'
|
{inputType === 'variable'
|
||||||
?
|
?
|
||||||
<VariableSelect
|
<VariableSelect
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-01-19 17:00:26
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-02-28 16:24:31
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* useVariableList Hook
|
||||||
|
*
|
||||||
|
* This hook provides functionality for managing and retrieving variables in workflow nodes.
|
||||||
|
* It handles variable extraction from different node types, including:
|
||||||
|
* - Node-specific output variables
|
||||||
|
* - Chat variables
|
||||||
|
* - Loop and iteration variables
|
||||||
|
* - Connected node variables
|
||||||
|
*/
|
||||||
import { useMemo, useEffect, useState } from 'react';
|
import { useMemo, useEffect, useState } from 'react';
|
||||||
import { Graph, Node } from '@antv/x6';
|
import { Graph, Node } from '@antv/x6';
|
||||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
|
||||||
import type { ChatVariable } from '../../../types';
|
import type { ChatVariable } from '../../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node variable definitions
|
||||||
|
*
|
||||||
|
* Maps node types to their available output variables
|
||||||
|
*/
|
||||||
const NODE_VARIABLES = {
|
const NODE_VARIABLES = {
|
||||||
llm: [{ label: 'output', dataType: 'string', field: 'output' }],
|
llm: [{ label: 'output', dataType: 'string', field: 'output' }],
|
||||||
'jinja-render': [{ label: 'output', dataType: 'string', field: 'output' }],
|
'jinja-render': [{ label: 'output', dataType: 'string', field: 'output' }],
|
||||||
@@ -23,6 +44,18 @@ const NODE_VARIABLES = {
|
|||||||
]
|
]
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add variable to list if not already present
|
||||||
|
*
|
||||||
|
* @param {Suggestion[]} list - List of suggestions to add to
|
||||||
|
* @param {Set<string>} keys - Set of existing keys to check for duplicates
|
||||||
|
* @param {string} key - Unique key for the variable
|
||||||
|
* @param {string} label - Human-readable label for the variable
|
||||||
|
* @param {string} dataType - Data type of the variable
|
||||||
|
* @param {string} value - Variable value/expression
|
||||||
|
* @param {any} nodeData - Node data associated with the variable
|
||||||
|
* @param {Partial<Suggestion>} [extra] - Additional suggestion properties
|
||||||
|
*/
|
||||||
const addVariable = (
|
const addVariable = (
|
||||||
list: Suggestion[],
|
list: Suggestion[],
|
||||||
keys: Set<string>,
|
keys: Set<string>,
|
||||||
@@ -39,6 +72,14 @@ const addVariable = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process node variables based on node type
|
||||||
|
*
|
||||||
|
* @param {any} nodeData - Node data object
|
||||||
|
* @param {string} dataNodeId - Node ID
|
||||||
|
* @param {Suggestion[]} variableList - List to add variables to
|
||||||
|
* @param {Set<string>} addedKeys - Set of already added keys
|
||||||
|
*/
|
||||||
const processNodeVariables = (
|
const processNodeVariables = (
|
||||||
nodeData: any,
|
nodeData: any,
|
||||||
dataNodeId: string,
|
dataNodeId: string,
|
||||||
@@ -47,29 +88,35 @@ const processNodeVariables = (
|
|||||||
) => {
|
) => {
|
||||||
const { type, config } = nodeData;
|
const { type, config } = nodeData;
|
||||||
|
|
||||||
|
// Add node-specific variables
|
||||||
if (type in NODE_VARIABLES) {
|
if (type in NODE_VARIABLES) {
|
||||||
NODE_VARIABLES[type as keyof typeof NODE_VARIABLES].forEach(({ label, dataType, field }) => {
|
NODE_VARIABLES[type as keyof typeof NODE_VARIABLES].forEach(({ label, dataType, field }) => {
|
||||||
addVariable(variableList, addedKeys, `${dataNodeId}_${label}`, label, dataType, `${dataNodeId}.${field}`, nodeData);
|
addVariable(variableList, addedKeys, `${dataNodeId}_${label}`, label, dataType, `${dataNodeId}.${field}`, nodeData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process special node types
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'start':
|
case 'start':
|
||||||
|
// Add start node variables
|
||||||
[...(config?.variables?.defaultValue ?? []), ...(config?.variables?.value ?? [])].forEach((v: any) => {
|
[...(config?.variables?.defaultValue ?? []), ...(config?.variables?.value ?? [])].forEach((v: any) => {
|
||||||
if (v?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${v.name}`, v.name, v.type, `${dataNodeId}.${v.name}`, nodeData);
|
if (v?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${v.name}`, v.name, v.type, `${dataNodeId}.${v.name}`, nodeData);
|
||||||
});
|
});
|
||||||
|
// Add system variables
|
||||||
config?.variables?.sys?.forEach((v: any) => {
|
config?.variables?.sys?.forEach((v: any) => {
|
||||||
if (v?.name) addVariable(variableList, addedKeys, `${dataNodeId}_sys_${v.name}`, `sys.${v.name}`, v.type, `sys.${v.name}`, nodeData);
|
if (v?.name) addVariable(variableList, addedKeys, `${dataNodeId}_sys_${v.name}`, `sys.${v.name}`, v.type, `sys.${v.name}`, nodeData);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'parameter-extractor':
|
case 'parameter-extractor':
|
||||||
|
// Add extracted parameters
|
||||||
(config?.params?.defaultValue || []).forEach((p: any) => {
|
(config?.params?.defaultValue || []).forEach((p: any) => {
|
||||||
if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData);
|
if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'var-aggregator':
|
case 'var-aggregator':
|
||||||
|
// Add aggregated variables
|
||||||
if (config.group.defaultValue) {
|
if (config.group.defaultValue) {
|
||||||
(config.group_variables.defaultValue || []).forEach((gv: any) => {
|
(config.group_variables.defaultValue || []).forEach((gv: any) => {
|
||||||
if (gv?.key) {
|
if (gv?.key) {
|
||||||
@@ -93,6 +140,7 @@ const processNodeVariables = (
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'iteration':
|
case 'iteration':
|
||||||
|
// Add iteration output variable
|
||||||
let dt = 'string';
|
let dt = 'string';
|
||||||
if (nodeData.output) {
|
if (nodeData.output) {
|
||||||
const sv = variableList.find(v => v.value === nodeData.output);
|
const sv = variableList.find(v => v.value === nodeData.output);
|
||||||
@@ -102,11 +150,14 @@ const processNodeVariables = (
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'loop':
|
case 'loop':
|
||||||
|
// Add loop cycle variables
|
||||||
(config.cycle_vars.defaultValue || []).forEach((cv: any) => {
|
(config.cycle_vars.defaultValue || []).forEach((cv: any) => {
|
||||||
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'code':
|
case 'code':
|
||||||
|
// Add code node output variables
|
||||||
(config.output_variables.defaultValue || []).forEach((cv: any) => {
|
(config.output_variables.defaultValue || []).forEach((cv: any) => {
|
||||||
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
||||||
});
|
});
|
||||||
@@ -114,6 +165,9 @@ const processNodeVariables = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node types that have output variables
|
||||||
|
*/
|
||||||
const hasOutputNodeTypes = [
|
const hasOutputNodeTypes = [
|
||||||
'llm',
|
'llm',
|
||||||
'knowledge-retrieval',
|
'knowledge-retrieval',
|
||||||
@@ -123,7 +177,15 @@ const hasOutputNodeTypes = [
|
|||||||
'http-request',
|
'http-request',
|
||||||
'tool',
|
'tool',
|
||||||
'jinja-render'
|
'jinja-render'
|
||||||
]
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get variables for the current node
|
||||||
|
*
|
||||||
|
* @param {any} nodeData - Node data object
|
||||||
|
* @param {any} values - Additional values to merge with node config
|
||||||
|
* @returns {Suggestion[]} List of node variables
|
||||||
|
*/
|
||||||
export const getCurrentNodeVariables = (nodeData: any, values: any): Suggestion[] => {
|
export const getCurrentNodeVariables = (nodeData: any, values: any): Suggestion[] => {
|
||||||
if (!nodeData || !hasOutputNodeTypes.includes(nodeData.type)) return [];
|
if (!nodeData || !hasOutputNodeTypes.includes(nodeData.type)) return [];
|
||||||
const list: Suggestion[] = [];
|
const list: Suggestion[] = [];
|
||||||
@@ -137,9 +199,18 @@ export const getCurrentNodeVariables = (nodeData: any, values: any): Suggestion[
|
|||||||
...values
|
...values
|
||||||
}
|
}
|
||||||
}, dataNodeId, list, keys);
|
}, dataNodeId, list, keys);
|
||||||
|
|
||||||
|
// Special case: var-aggregator without group enabled returns no variables
|
||||||
return nodeData.type === 'var-aggregator' && !nodeData.config.group.defaultValue ? [] : list;
|
return nodeData.type === 'var-aggregator' && !nodeData.config.group.defaultValue ? [] : list;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get variables from child nodes in a loop/iteration
|
||||||
|
*
|
||||||
|
* @param {Node} selectedNode - Selected node
|
||||||
|
* @param {React.MutableRefObject<Graph | undefined>} graphRef - Graph reference
|
||||||
|
* @returns {Suggestion[]} List of child node variables
|
||||||
|
*/
|
||||||
export const getChildNodeVariables = (
|
export const getChildNodeVariables = (
|
||||||
selectedNode: Node,
|
selectedNode: Node,
|
||||||
graphRef: React.MutableRefObject<Graph | undefined>
|
graphRef: React.MutableRefObject<Graph | undefined>
|
||||||
@@ -152,8 +223,15 @@ export const getChildNodeVariables = (
|
|||||||
const edges = graph.getEdges();
|
const edges = graph.getEdges();
|
||||||
const keys = new Set<string>();
|
const keys = new Set<string>();
|
||||||
|
|
||||||
|
// Find child nodes in the same cycle
|
||||||
const childNodes = nodes.filter(node => node.getData()?.cycle === selectedNode.id);
|
const childNodes = nodes.filter(node => node.getData()?.cycle === selectedNode.id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all connected nodes recursively
|
||||||
|
* @param {string} nodeId - Node ID to start from
|
||||||
|
* @param {Set<string>} visited - Set of visited node IDs
|
||||||
|
* @returns {string[]} List of connected node IDs
|
||||||
|
*/
|
||||||
const getConnectedNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
const getConnectedNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
||||||
if (visited.has(nodeId)) return [];
|
if (visited.has(nodeId)) return [];
|
||||||
visited.add(nodeId);
|
visited.add(nodeId);
|
||||||
@@ -161,12 +239,14 @@ export const getChildNodeVariables = (
|
|||||||
return [...prev, ...prev.flatMap(id => getConnectedNodes(id, visited))];
|
return [...prev, ...prev.flatMap(id => getConnectedNodes(id, visited))];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Collect all relevant node IDs
|
||||||
const relevantIds = new Set<string>();
|
const relevantIds = new Set<string>();
|
||||||
childNodes.forEach(child => {
|
childNodes.forEach(child => {
|
||||||
relevantIds.add(child.id);
|
relevantIds.add(child.id);
|
||||||
getConnectedNodes(child.id).forEach(id => relevantIds.add(id));
|
getConnectedNodes(child.id).forEach(id => relevantIds.add(id));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Process each relevant node
|
||||||
relevantIds.forEach(id => {
|
relevantIds.forEach(id => {
|
||||||
const node = nodes.find(n => n.id === id);
|
const node = nodes.find(n => n.id === id);
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
@@ -175,6 +255,7 @@ export const getChildNodeVariables = (
|
|||||||
const nodeId = nodeData.id;
|
const nodeId = nodeData.id;
|
||||||
const { type } = nodeData;
|
const { type } = nodeData;
|
||||||
|
|
||||||
|
// Add node-specific variables
|
||||||
if (type in NODE_VARIABLES) {
|
if (type in NODE_VARIABLES) {
|
||||||
NODE_VARIABLES[type as keyof typeof NODE_VARIABLES].forEach(({ label, dataType, field }) => {
|
NODE_VARIABLES[type as keyof typeof NODE_VARIABLES].forEach(({ label, dataType, field }) => {
|
||||||
const varKey = `${nodeId}_${label}`;
|
const varKey = `${nodeId}_${label}`;
|
||||||
@@ -192,6 +273,7 @@ export const getChildNodeVariables = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add parameter-extractor variables
|
||||||
if (type === 'parameter-extractor') {
|
if (type === 'parameter-extractor') {
|
||||||
(nodeData.config?.params?.defaultValue || []).forEach((p: any) => {
|
(nodeData.config?.params?.defaultValue || []).forEach((p: any) => {
|
||||||
if (p?.name && !keys.has(`${nodeId}_${p.name}`)) {
|
if (p?.name && !keys.has(`${nodeId}_${p.name}`)) {
|
||||||
@@ -207,11 +289,36 @@ export const getChildNodeVariables = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add code node variables
|
||||||
|
if (type === 'code') {
|
||||||
|
(nodeData.config?.output_variables?.defaultValue || []).forEach((p: any) => {
|
||||||
|
if (p?.name && !keys.has(`${nodeId}_${p.name}`)) {
|
||||||
|
keys.add(`${nodeId}_${p.name}`);
|
||||||
|
list.push({
|
||||||
|
key: `${nodeId}_${p.name}`,
|
||||||
|
label: p.name,
|
||||||
|
type: 'variable',
|
||||||
|
dataType: p.type || 'string',
|
||||||
|
value: `${nodeId}.${p.name}`,
|
||||||
|
nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing workflow variable list
|
||||||
|
*
|
||||||
|
* @param {Node | null | undefined} selectedNode - Currently selected node
|
||||||
|
* @param {React.MutableRefObject<Graph | undefined>} graphRef - Graph reference
|
||||||
|
* @param {ChatVariable[]} chatVariables - List of chat variables
|
||||||
|
* @returns {Suggestion[]} List of available variables
|
||||||
|
*/
|
||||||
export const useVariableList = (
|
export const useVariableList = (
|
||||||
selectedNode: Node | null | undefined,
|
selectedNode: Node | null | undefined,
|
||||||
graphRef: React.MutableRefObject<Graph | undefined>,
|
graphRef: React.MutableRefObject<Graph | undefined>,
|
||||||
@@ -228,6 +335,12 @@ export const useVariableList = (
|
|||||||
const nodes = graph.getNodes();
|
const nodes = graph.getNodes();
|
||||||
const keys = new Set<string>();
|
const keys = new Set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all previous connected nodes recursively
|
||||||
|
* @param {string} nodeId - Node ID to start from
|
||||||
|
* @param {Set<string>} visited - Set of visited node IDs
|
||||||
|
* @returns {string[]} List of previous node IDs
|
||||||
|
*/
|
||||||
const getPreviousNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
const getPreviousNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
||||||
if (visited.has(nodeId)) return [];
|
if (visited.has(nodeId)) return [];
|
||||||
visited.add(nodeId);
|
visited.add(nodeId);
|
||||||
@@ -235,6 +348,11 @@ export const useVariableList = (
|
|||||||
return [...prev, ...prev.flatMap(id => getPreviousNodes(id, visited))];
|
return [...prev, ...prev.flatMap(id => getPreviousNodes(id, visited))];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get parent loop/iteration node
|
||||||
|
* @param {string} nodeId - Node ID to check
|
||||||
|
* @returns {Node | null} Parent loop/iteration node or null
|
||||||
|
*/
|
||||||
const getParentLoop = (nodeId: string): Node | null => {
|
const getParentLoop = (nodeId: string): Node | null => {
|
||||||
const node = nodes.find(n => n.id === nodeId);
|
const node = nodes.find(n => n.id === nodeId);
|
||||||
const cycle = node?.getData()?.cycle;
|
const cycle = node?.getData()?.cycle;
|
||||||
@@ -245,17 +363,21 @@ export const useVariableList = (
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Collect relevant node IDs
|
||||||
const childIds = nodes.filter(n => n.getData()?.cycle === selectedNode.id).map(n => n.id);
|
const childIds = nodes.filter(n => n.getData()?.cycle === selectedNode.id).map(n => n.id);
|
||||||
const parentLoop = getParentLoop(selectedNode.id);
|
const parentLoop = getParentLoop(selectedNode.id);
|
||||||
const relevantIds = [...getPreviousNodes(selectedNode.id), ...childIds, ...(parentLoop ? getPreviousNodes(parentLoop.id) : [])];
|
const relevantIds = [...getPreviousNodes(selectedNode.id), ...childIds, ...(parentLoop ? getPreviousNodes(parentLoop.id) : [])];
|
||||||
|
|
||||||
|
// Add chat variables
|
||||||
chatVariables?.forEach(v => addVariable(list, keys, `CONVERSATION_${v.name}`, v.name, v.type, `conv.${v.name}`, { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, { group: 'CONVERSATION' }));
|
chatVariables?.forEach(v => addVariable(list, keys, `CONVERSATION_${v.name}`, v.name, v.type, `conv.${v.name}`, { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, { group: 'CONVERSATION' }));
|
||||||
|
|
||||||
|
// Process each relevant node
|
||||||
relevantIds.forEach(id => {
|
relevantIds.forEach(id => {
|
||||||
const node = nodes.find(n => n.id === id);
|
const node = nodes.find(n => n.id === id);
|
||||||
if (node) processNodeVariables(node.getData(), node.getData().id, list, keys);
|
if (node) processNodeVariables(node.getData(), node.getData().id, list, keys);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add parent loop variables
|
||||||
if (parentLoop) {
|
if (parentLoop) {
|
||||||
const pd = parentLoop.getData();
|
const pd = parentLoop.getData();
|
||||||
const pid = pd.id;
|
const pid = pd.id;
|
||||||
@@ -270,7 +392,9 @@ export const useVariableList = (
|
|||||||
} else if (pd.type === 'iteration' && !pd.config.input.defaultValue) {
|
} else if (pd.type === 'iteration' && !pd.config.input.defaultValue) {
|
||||||
let itemType = 'object';
|
let itemType = 'object';
|
||||||
const iv = list.find(v => `{{${v.value}}}` === pd.config.input.defaultValue);
|
const iv = list.find(v => `{{${v.value}}}` === pd.config.input.defaultValue);
|
||||||
if (iv?.dataType.startsWith('array[')) {itemType = iv.dataType.replace(/^array\[(.+)\]$/, '$1');}
|
if (iv?.dataType.startsWith('array[')) {
|
||||||
|
itemType = iv.dataType.replace(/^array\[(.+)\]$/, '$1');
|
||||||
|
}
|
||||||
addVariable(list, keys, `${pid}_item`, 'item', 'string', `${pid}.item`, pd);
|
addVariable(list, keys, `${pid}_item`, 'item', 'string', `${pid}.item`, pd);
|
||||||
addVariable(list, keys, `${pid}_index`, 'index', 'number', `${pid}.index`, pd);
|
addVariable(list, keys, `${pid}_index`, 'index', 'number', `${pid}.index`, pd);
|
||||||
}
|
}
|
||||||
@@ -279,6 +403,7 @@ export const useVariableList = (
|
|||||||
return list;
|
return list;
|
||||||
}, [selectedNode, graphRef, trigger, chatVariables]);
|
}, [selectedNode, graphRef, trigger, chatVariables]);
|
||||||
|
|
||||||
|
// Refresh variable list when graph changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!graphRef?.current) return;
|
if (!graphRef?.current) return;
|
||||||
const graph = graphRef.current;
|
const graph = graphRef.current;
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
config: {
|
config: {
|
||||||
input: {
|
input: {
|
||||||
type: 'variableList',
|
type: 'variableList',
|
||||||
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor', 'code'],
|
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor', 'code', 'CONVERSATION'],
|
||||||
filterVariableNames: ['message']
|
filterVariableNames: ['message']
|
||||||
},
|
},
|
||||||
parallel: {
|
parallel: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 15:17:48
|
* @Date: 2026-02-03 15:17:48
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-09 18:37:01
|
* @Last Modified time: 2026-02-28 17:59:34
|
||||||
*/
|
*/
|
||||||
import { useRef, useEffect, useState } from 'react';
|
import { useRef, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@@ -135,7 +135,24 @@ export const useWorkflowGraph = ({
|
|||||||
|
|
||||||
if (nodeLibraryConfig?.config) {
|
if (nodeLibraryConfig?.config) {
|
||||||
Object.keys(nodeLibraryConfig.config).forEach(key => {
|
Object.keys(nodeLibraryConfig.config).forEach(key => {
|
||||||
if (type === 'memory-write' && key === 'message' && nodeLibraryConfig.config) {
|
if (type === 'loop' && key === 'condition' && nodeLibraryConfig.config) {
|
||||||
|
const { condition } = config;
|
||||||
|
console.log('condition', condition)
|
||||||
|
nodeLibraryConfig.config[key].defaultValue = condition ? {
|
||||||
|
...condition,
|
||||||
|
expressions: (condition as any).expressions.map((expr: any) => {
|
||||||
|
return expr.input_type ? { ...expr, input_type: expr.input_type.toLocaleLowerCase() } : expr
|
||||||
|
})
|
||||||
|
} : {}
|
||||||
|
} else if (type === 'if-else' && key === 'cases' && nodeLibraryConfig.config) {
|
||||||
|
const { cases } = config;
|
||||||
|
nodeLibraryConfig.config[key].defaultValue = cases && Array.isArray(cases) ? cases.map(item => ({
|
||||||
|
...item,
|
||||||
|
expressions: item.expressions.map((expr: any) => {
|
||||||
|
return expr.input_type ? { ...expr, input_type: expr.input_type.toLocaleLowerCase() } : expr
|
||||||
|
}),
|
||||||
|
})) : []
|
||||||
|
} else if (type === 'memory-write' && key === 'message' && nodeLibraryConfig.config) {
|
||||||
nodeLibraryConfig.config['messages'].defaultValue = [{ role: 'USER', content: config[key] }]
|
nodeLibraryConfig.config['messages'].defaultValue = [{ role: 'USER', content: config[key] }]
|
||||||
delete nodeLibraryConfig.config[key]
|
delete nodeLibraryConfig.config[key]
|
||||||
} else if (key === 'memory' && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
} else if (key === 'memory' && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
|||||||
handleSave,
|
handleSave,
|
||||||
handleRun,
|
handleRun,
|
||||||
graphRef,
|
graphRef,
|
||||||
addVariable
|
addVariable,
|
||||||
|
config
|
||||||
}))
|
}))
|
||||||
return (
|
return (
|
||||||
<div className="rb:h-[calc(100vh-64px)] rb:relative">
|
<div className="rb:h-[calc(100vh-64px)] rb:relative">
|
||||||
|
|||||||
Reference in New Issue
Block a user