Merge pull request #440 from SuanmoSuanyangTechnology/feature/workflow_import_zy

Feature/workflow import zy
This commit is contained in:
yingzhao
2026-03-03 15:33:13 +08:00
committed by GitHub
11 changed files with 140 additions and 29 deletions

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 13:59:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-28 16:34:15
* @Last Modified time: 2026-03-03 12:08:42
*/
import { request } from '@/utils/request'
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
@@ -120,15 +120,19 @@ export const copyApplication = (app_id: string, new_name: string) => {
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
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')
}
// 工作流上传+兼容性分析
// Upload workflow and analyze compatibility
export const importWorkflow = (formData: FormData) => {
return request.uploadFile(`/apps/workflow/import`, formData)
}
// 完成工作流导入
// Complete workflow import
export const completeImportWorkflow = (data: { temp_id: string; name?: string; description?: string }) => {
return request.post(`/apps/workflow/import/save`, data)
}
// Get experience config
export const getExperienceConfig = (share_token: string) => {
return request.get(`/public/share/config`, {}, {
headers: {
'Authorization': `Bearer ${localStorage.getItem(`shareToken_${share_token}`)}`
}
})
}

View File

@@ -1685,7 +1685,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
fileType: 'File Type',
image: 'Image',
fileUrl: 'File URL',
addRemoteFile: 'Add Remote File'
addRemoteFile: 'Add Remote File',
variableConfig: 'Variable Configuration',
},
login: {
title: 'Red Bear Memory Science',
@@ -2193,7 +2194,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
save: 'Save',
export: 'Export',
variableConfig: 'Variable Configuration',
variableRequired: 'Required',
variableRequired: 'Required, please configure variable values',
addMessage: 'Add Message',
answerDesc: 'Reply',
addNode: 'Add Node',

View File

@@ -1682,7 +1682,8 @@ export const zh = {
fileType: '文件类型',
image: '图片',
fileUrl: '文件链接',
addRemoteFile: '添加远程文件'
addRemoteFile: '添加远程文件',
variableConfig: '变量配置',
},
login: {
title: '红熊记忆科学',
@@ -2193,7 +2194,7 @@ export const zh = {
save: '保存',
export: '导出',
variableConfig: '变量配置',
variableRequired: '必填',
variableRequired: '必填,请配置变量值',
addMessage: '添加消息',
answerDesc: '回复',
addNode: '添加节点',

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:21
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 11:14:30
* @Last Modified time: 2026-03-03 14:24:34
*/
import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx'
@@ -403,6 +403,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
const handleSaveChatVariable = (values: Variable[]) => {
setChatVariables(values)
}
useEffect(() => {
setChatVariables(values?.variables || [])
}, [values?.variables])
console.log('values', values)
return (
<>
@@ -507,6 +510,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
chatList={chatList}
updateChatList={setChatList}
handleSave={handleSave}
chatVariables={chatVariables}
/>
</RbCard>
</Col>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:39
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 17:40:15
* @Last Modified time: 2026-03-03 14:21:54
*/
/**
* Chat debugging component for application testing
@@ -13,7 +13,7 @@
import { type FC, useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx'
import { Flex, Dropdown, type MenuProps } from 'antd'
import { Flex, Dropdown, type MenuProps, App } from 'antd'
import ChatIcon from '@/assets/images/application/chat.png'
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
@@ -28,6 +28,7 @@ import UploadFiles from '@/views/Conversation/components/FileUpload'
// import AudioRecorder from '@/components/AudioRecorder'
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
import type { UploadFileListModalRef } from '@/views/Conversation/types'
import type { Variable } from './VariableList/types'
/**
* Component props
@@ -43,14 +44,16 @@ interface ChatProps {
handleSave: (flag?: boolean) => Promise<unknown>;
/** Source type: multi-agent cluster or single agent */
source?: 'multi_agent' | 'agent';
chatVariables?: Variable[]; // Add chatVariables prop
}
/**
* Chat debugging component
* Allows testing application with different model configurations side-by-side
*/
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => {
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent', chatVariables }) => {
const { t } = useTranslation();
const { message: messageApi } = App.useApp()
const [loading, setLoading] = useState(false)
const [isCluster, setIsCluster] = useState(source === 'multi_agent')
const [conversationId, setConversationId] = useState<string | null>(null)
@@ -195,6 +198,27 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
};
setTimeout(() => {
// Validate required variables before sending
let isCanSend = true
const params: Record<string, any> = {}
if (chatVariables && chatVariables.length > 0) {
const needRequired: string[] = []
chatVariables.forEach(vo => {
params[vo.name] = vo.value
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
isCanSend = false
needRequired.push(vo.name)
}
})
if (needRequired.length) {
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
}
}
if (!isCanSend) {
return
}
runCompare(data.app_id, {
message,
files: fileList.map(file => {
@@ -214,7 +238,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
model_parameters: item.model_parameters,
conversation_id: item.conversation_id
})),
variables: {},
variables: params,
"parallel": true,
"stream": true,
"timeout": 60,

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:28:46
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:28:46
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 14:03:44
*/
/**
* Release Share Modal
@@ -79,7 +79,7 @@ const ReleaseShareModal = forwardRef<ReleaseShareModalRef, ReleaseShareModalProp
return (
<RbModal
title={<>{t('application.shareVersion')} {version?.version}</>}
title={<>{t('application.shareVersion')} ({version?.version_name && version.version_name[0].toLocaleLowerCase() === 'v' ? version.version_name : version?.version_name ? `v${version.version_name}` : `v${version?.version}`})</>}
open={visible}
onCancel={handleClose}
footer={false}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:58:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 17:41:05
* @Last Modified time: 2026-03-03 13:46:22
*/
/**
* Conversation Page
@@ -14,11 +14,12 @@ import { type FC, useState, useEffect, useRef } from 'react'
import { useParams, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import InfiniteScroll from 'react-infinite-scroll-component';
import { Flex, Skeleton, Form, Dropdown, type MenuProps } from 'antd'
import { Flex, Skeleton, Form, Dropdown, type MenuProps, App } from 'antd'
import { SettingOutlined } from '@ant-design/icons'
import clsx from 'clsx'
import dayjs from 'dayjs'
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken } from '@/api/application'
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken, getExperienceConfig } from '@/api/application'
import type { HistoryItem, QueryParams, UploadFileListModalRef } from './types'
import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format';
@@ -37,12 +38,16 @@ import UploadFiles from './components/FileUpload'
// import AudioRecorder from '@/components/AudioRecorder'
import { shareFileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
import UploadFileListModal from './components/UploadFileListModal'
import type { VariableConfigModalRef } from '@/views/Workflow/types'
import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types'
import VariableConfigModal from '@/views/Workflow/components/Chat/VariableConfigModal';
/**
* Conversation component for shared applications
*/
const Conversation: FC = () => {
const { t } = useTranslation()
const { message: messageApi } = App.useApp()
const { token } = useParams()
const location = useLocation()
const searchParams = new URLSearchParams(location.search)
@@ -64,6 +69,22 @@ const Conversation: FC = () => {
const queryValues = Form.useWatch<QueryParams>([], form)
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
const variableConfigModalRef = useRef<VariableConfigModalRef>(null)
const [variables, setVariables] = useState<Variable[]>([]) // Workflow input variables
/**
* Opens the variable configuration modal
*/
const handleEditVariables = () => {
variableConfigModalRef.current?.handleOpen(variables)
}
/**
* Saves updated variable values from the modal
*/
const handleSave = (values: Variable[]) => {
setVariables([...values])
}
useEffect(() => {
const shareToken = localStorage.getItem(`shareToken_${token}`)
setShareToken(shareToken)
@@ -81,6 +102,17 @@ const Conversation: FC = () => {
getHistory()
}
}, [token, shareToken, page, hasMore, historyList])
useEffect(() => {
if (shareToken && token) {
getExperienceConfig(token)
.then(res => {
const response = res as { variables: Variable[] }
setVariables(response.variables || [])
})
} else {
setChatList([])
}
}, [shareToken, token])
/** Group conversation history by date */
const groupHistoryByDate = (items: HistoryItem[]): Record<string, HistoryItem[]> => {
@@ -191,12 +223,35 @@ const Conversation: FC = () => {
})
}
const isNeedVariableConfig = variables.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === ''))
/** Send message and handle streaming response */
const handleSend = () => {
if (!token || !shareToken) {
return
}
const { files = [], ...rest } = queryValues || {}
// Validate required variables before sending
let isCanSend = true
const params: Record<string, any> = {}
if (variables.length > 0) {
const needRequired: string[] = []
variables.forEach(vo => {
params[vo.name] = vo.value ?? vo.defaultValue
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
isCanSend = false
needRequired.push(vo.name)
}
})
if (needRequired.length) {
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
}
}
if (!isCanSend) {
return
}
setLoading(true)
setStreamLoading(true)
addUserMessage(message, files)
@@ -247,7 +302,8 @@ const Conversation: FC = () => {
upload_file_id: file.response.data.file_id
}
}
})
}),
variables: params
}, handleStreamMessage, shareToken)
.finally(() => {
setLoading(false)
@@ -384,6 +440,20 @@ const Conversation: FC = () => {
{t(`memoryConversation.memory`)}
</ButtonCheckbox>
</Form.Item>
{variables.length > 0 && (
<Form.Item name="variables" className="rb:mb-0!">
<div
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]", {
'rb:border-[#FF5D34] rb:text-[#FF5D34]': isNeedVariableConfig,
'rb:border-[#DFE4ED]': !isNeedVariableConfig,
})}
onClick={handleEditVariables}
>
<SettingOutlined className="rb:mr-1" />
{t(`memoryConversation.variableConfig`)}
</div>
</Form.Item>
)}
</Flex>
{/* <Flex align="center">
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
@@ -399,6 +469,11 @@ const Conversation: FC = () => {
ref={uploadFileListModalRef}
refresh={addFileList}
/>
<VariableConfigModal
ref={variableConfigModalRef}
refresh={handleSave}
variables={variables}
/>
</Flex>
)
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:57:46
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-06 21:11:19
* @Last Modified time: 2026-03-03 13:46:55
*/
/**
* Type definitions for Conversation
@@ -51,6 +51,7 @@ export interface QueryParams {
/** Current conversation ID */
conversation_id?: string | null;
files?: any[];
variables?: Record<string, any>;
}
export interface UploadFileListModalRef {

View File

@@ -71,20 +71,20 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
<Form.Item
key={name}
name={[name, 'value']}
label={field.type === 'boolean' ? undefined : `${field.name}·${field.description}`}
label={field.type === 'boolean' ? undefined : `${field.name}·${field.display_name || field.description}`}
valuePropName={field.type === 'boolean' ? 'checked' : 'value'}
rules={[
{ required: field.required, message: field.type === 'boolean' ? t('common.pleaseSelect') : t('common.pleaseEnter') },
]}
>
{
field.type === 'string' && <Input placeholder={t('common.pleaseEnter')} />
(field.type === 'string' || field.type === 'text') && <Input placeholder={t('common.pleaseEnter')} />
}
{
field.type === 'number' && <InputNumber placeholder={t('common.pleaseEnter')} style={{ width: '100%' }} onChange={(value) => form.setFieldValue(['variables', name, 'value'], value)} />
}
{
field.type === 'boolean' && <Checkbox>{`${field.name}·${field.description}`}</Checkbox>
field.type === 'boolean' && <Checkbox>{`${field.name}·${field.display_name || field.description}`}</Checkbox>
}
</Form.Item>
)

View File

@@ -59,7 +59,7 @@ const EditableTable: FC<EditableTableProps> = ({
render: (_: any, __: TableRow, index: number) => (
<Form.Item name={[index, 'name']} noStyle>
<Editor
options={booleanFilterOptions}
options={booleanFilterOptions.filter(option => !option.dataType.includes('file'))}
type="input"
className={contentClassName}
size={size}
@@ -109,7 +109,7 @@ const EditableTable: FC<EditableTableProps> = ({
const currentType = form.getFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'type']);
const filteredOptions = currentType === 'file'
? booleanFilterOptions.filter(option => option.dataType.includes('file'))
: booleanFilterOptions;
: booleanFilterOptions.filter(option => !option.dataType.includes('file'));
return (
<Form.Item name={[index, 'value']} noStyle>

View File

@@ -1,5 +1,6 @@
export interface Variable {
name: string;
display_name?: string;
type: string;
required: boolean;
description: string;