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 * @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-28 16:34:15 * @Last Modified time: 2026-03-03 12:08:42
*/ */
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,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; }) => { 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)
} }
// 导出工作流 // Upload workflow and analyze compatibility
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) => { export const importWorkflow = (formData: FormData) => {
return request.uploadFile(`/apps/workflow/import`, formData) return request.uploadFile(`/apps/workflow/import`, formData)
} }
// 完成工作流导入 // Complete workflow import
export const completeImportWorkflow = (data: { temp_id: string; name?: string; description?: string }) => { export const completeImportWorkflow = (data: { temp_id: string; name?: string; description?: string }) => {
return request.post(`/apps/workflow/import/save`, data) 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', fileType: 'File Type',
image: 'Image', image: 'Image',
fileUrl: 'File URL', fileUrl: 'File URL',
addRemoteFile: 'Add Remote File' addRemoteFile: 'Add Remote File',
variableConfig: 'Variable Configuration',
}, },
login: { login: {
title: 'Red Bear Memory Science', title: 'Red Bear Memory Science',
@@ -2193,7 +2194,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
save: 'Save', save: 'Save',
export: 'Export', export: 'Export',
variableConfig: 'Variable Configuration', variableConfig: 'Variable Configuration',
variableRequired: 'Required', variableRequired: 'Required, please configure variable values',
addMessage: 'Add Message', addMessage: 'Add Message',
answerDesc: 'Reply', answerDesc: 'Reply',
addNode: 'Add Node', addNode: 'Add Node',

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:27:39 * @Date: 2026-02-03 16:27:39
* @Last Modified by: ZhaoYing * @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 * Chat debugging component for application testing
@@ -13,7 +13,7 @@
import { type FC, useEffect, useState, useRef } from 'react'; import { type FC, useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import clsx from 'clsx' 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 ChatIcon from '@/assets/images/application/chat.png'
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.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 AudioRecorder from '@/components/AudioRecorder'
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal' import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
import type { UploadFileListModalRef } from '@/views/Conversation/types' import type { UploadFileListModalRef } from '@/views/Conversation/types'
import type { Variable } from './VariableList/types'
/** /**
* Component props * Component props
@@ -43,14 +44,16 @@ interface ChatProps {
handleSave: (flag?: boolean) => Promise<unknown>; handleSave: (flag?: boolean) => Promise<unknown>;
/** Source type: multi-agent cluster or single agent */ /** Source type: multi-agent cluster or single agent */
source?: 'multi_agent' | 'agent'; source?: 'multi_agent' | 'agent';
chatVariables?: Variable[]; // Add chatVariables prop
} }
/** /**
* Chat debugging component * Chat debugging component
* Allows testing application with different model configurations side-by-side * 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 { t } = useTranslation();
const { message: messageApi } = App.useApp()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [isCluster, setIsCluster] = useState(source === 'multi_agent') const [isCluster, setIsCluster] = useState(source === 'multi_agent')
const [conversationId, setConversationId] = useState<string | null>(null) const [conversationId, setConversationId] = useState<string | null>(null)
@@ -195,6 +198,27 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
}; };
setTimeout(() => { 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, { runCompare(data.app_id, {
message, message,
files: fileList.map(file => { files: fileList.map(file => {
@@ -214,7 +238,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
model_parameters: item.model_parameters, model_parameters: item.model_parameters,
conversation_id: item.conversation_id conversation_id: item.conversation_id
})), })),
variables: {}, variables: params,
"parallel": true, "parallel": true,
"stream": true, "stream": true,
"timeout": 60, "timeout": 60,

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:28:46 * @Date: 2026-02-03 16:28:46
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:28:46 * @Last Modified time: 2026-03-03 14:03:44
*/ */
/** /**
* Release Share Modal * Release Share Modal
@@ -79,7 +79,7 @@ const ReleaseShareModal = forwardRef<ReleaseShareModalRef, ReleaseShareModalProp
return ( return (
<RbModal <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} open={visible}
onCancel={handleClose} onCancel={handleClose}
footer={false} footer={false}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:58:03 * @Date: 2026-02-03 16:58:03
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 17:41:05 * @Last Modified time: 2026-03-03 13:46:22
*/ */
/** /**
* Conversation Page * Conversation Page
@@ -14,11 +14,12 @@ import { type FC, useState, useEffect, useRef } from 'react'
import { useParams, useLocation } from 'react-router-dom' import { useParams, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import InfiniteScroll from 'react-infinite-scroll-component'; 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 clsx from 'clsx'
import dayjs from 'dayjs' 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 type { HistoryItem, QueryParams, UploadFileListModalRef } from './types'
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
@@ -37,12 +38,16 @@ import UploadFiles from './components/FileUpload'
// import AudioRecorder from '@/components/AudioRecorder' // import AudioRecorder from '@/components/AudioRecorder'
import { shareFileUploadUrlWithoutApiPrefix } from '@/api/fileStorage' import { shareFileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
import UploadFileListModal from './components/UploadFileListModal' 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 * Conversation component for shared applications
*/ */
const Conversation: FC = () => { const Conversation: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { message: messageApi } = App.useApp()
const { token } = useParams() const { token } = useParams()
const location = useLocation() const location = useLocation()
const searchParams = new URLSearchParams(location.search) const searchParams = new URLSearchParams(location.search)
@@ -64,6 +69,22 @@ const Conversation: FC = () => {
const queryValues = Form.useWatch<QueryParams>([], form) const queryValues = Form.useWatch<QueryParams>([], form)
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null) 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(() => { useEffect(() => {
const shareToken = localStorage.getItem(`shareToken_${token}`) const shareToken = localStorage.getItem(`shareToken_${token}`)
setShareToken(shareToken) setShareToken(shareToken)
@@ -81,6 +102,17 @@ const Conversation: FC = () => {
getHistory() getHistory()
} }
}, [token, shareToken, page, hasMore, historyList]) }, [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 */ /** Group conversation history by date */
const groupHistoryByDate = (items: HistoryItem[]): Record<string, HistoryItem[]> => { 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 */ /** Send message and handle streaming response */
const handleSend = () => { const handleSend = () => {
if (!token || !shareToken) { if (!token || !shareToken) {
return return
} }
const { files = [], ...rest } = queryValues || {} 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) setLoading(true)
setStreamLoading(true) setStreamLoading(true)
addUserMessage(message, files) addUserMessage(message, files)
@@ -247,7 +302,8 @@ const Conversation: FC = () => {
upload_file_id: file.response.data.file_id upload_file_id: file.response.data.file_id
} }
} }
}) }),
variables: params
}, handleStreamMessage, shareToken) }, handleStreamMessage, shareToken)
.finally(() => { .finally(() => {
setLoading(false) setLoading(false)
@@ -384,6 +440,20 @@ const Conversation: FC = () => {
{t(`memoryConversation.memory`)} {t(`memoryConversation.memory`)}
</ButtonCheckbox> </ButtonCheckbox>
</Form.Item> </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>
{/* <Flex align="center"> {/* <Flex align="center">
<AudioRecorder onRecordingComplete={handleRecordingComplete} /> <AudioRecorder onRecordingComplete={handleRecordingComplete} />
@@ -399,6 +469,11 @@ const Conversation: FC = () => {
ref={uploadFileListModalRef} ref={uploadFileListModalRef}
refresh={addFileList} refresh={addFileList}
/> />
<VariableConfigModal
ref={variableConfigModalRef}
refresh={handleSave}
variables={variables}
/>
</Flex> </Flex>
) )
} }

View File

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

View File

@@ -71,20 +71,20 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
<Form.Item <Form.Item
key={name} key={name}
name={[name, 'value']} 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'} valuePropName={field.type === 'boolean' ? 'checked' : 'value'}
rules={[ rules={[
{ required: field.required, message: field.type === 'boolean' ? t('common.pleaseSelect') : t('common.pleaseEnter') }, { 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 === '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> </Form.Item>
) )

View File

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

View File

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