Merge branch 'develop' into feature/ui_upgrade_zy

This commit is contained in:
zhaoying
2026-03-20 11:49:00 +08:00
286 changed files with 23406 additions and 5328 deletions

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:21
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 10:28:59
* @Last Modified time: 2026-03-20 11:36:49
*/
import { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import { useTranslation } from 'react-i18next'
@@ -22,7 +22,8 @@ import type {
MemoryConfig,
AiPromptModalRef,
Source,
ChatVariableConfigModalRef
ChatVariableConfigModalRef,
FeaturesConfigForm
} from './types'
import type { Variable } from './components/VariableList/types'
import type { KnowledgeConfig } from './components/Knowledge/types'
@@ -41,12 +42,13 @@ import ChatVariableConfigModal from './components/ChatVariableConfigModal';
import type { Skill } from '@/views/Skills/types'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
import DescWrapper from '@/components/FormItem/DescWrapper'
import FeaturesConfig from './components/FeaturesConfig'
/**
* Agent configuration component
* Manages single agent configuration including prompts, knowledge, memory, variables, and tools
*/
const Agent = forwardRef<AgentRef>((_props, ref) => {
const Agent = forwardRef<AgentRef, { onFeaturesLoad?: (features: FeaturesConfigForm | undefined) => void }>(({ onFeaturesLoad }, ref) => {
const { t } = useTranslation()
const { id } = useParams();
const { message } = App.useApp()
@@ -117,6 +119,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
...response,
tools: allTools
})
onFeaturesLoad?.(response.features)
}).finally(() => {
setLoading(false)
})
@@ -272,7 +275,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
}, [modelList, values?.default_model_config_id])
useImperativeHandle(ref, () => ({
handleSave
handleSave,
features: values?.features
}))
const aiPromptModalRef = useRef<AiPromptModalRef>(null)
@@ -326,6 +330,10 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
useEffect(() => {
setChatVariables(values?.variables || [])
}, [values?.variables])
const handleSaveFeaturesConfig = (value: FeaturesConfigForm) => {
form.setFieldValue('features', value)
}
console.log('values', values)
return (
<>
@@ -339,13 +347,16 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
{defaultModel?.name ? <div className="rb:size-4 rb:bg-[url('@/assets/images/application/model.svg')]"></div> : null}
{defaultModel?.name || t('application.chooseModel')}
</Button>
<Button type="primary" onClick={() => handleSave()}>
{t('common.save')}
</Button>
<Space size={12}>
<FeaturesConfig value={values?.features as FeaturesConfigForm} refresh={handleSaveFeaturesConfig} />
<Button type="primary" onClick={() => handleSave()}>
{t('common.save')}
</Button>
</Space>
</Flex>
<Form.Item name="default_model_config_id" hidden noStyle></Form.Item>
<Form.Item name="model_parameters" hidden noStyle></Form.Item>
<Form.Item name="features" hidden noStyle></Form.Item>
<Card
title={t('application.promptConfiguration')}
extra={
@@ -364,25 +375,25 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
<span className="rb:font-regular rb:text-[#5B6167]"> ({t('application.configurationDesc')})</span>
</div>
<Form.Item name="system_prompt" className="rb:mb-0!">
<Input.TextArea
placeholder={t('application.promptPlaceholder')}
styles={{
textarea: {
minHeight: '200px',
borderRadius: '8px',
padding: '12px'
},
}}
/>
<Form.Item name="system_prompt" className="rb:mb-0!">
<Input.TextArea
placeholder={t('application.promptPlaceholder')}
styles={{
textarea: {
minHeight: '200px',
borderRadius: '8px',
padding: '12px'
},
}}
/>
</Form.Item>
</Card>
<Form.Item name="knowledge_retrieval" noStyle>
<Knowledge />
</Form.Item>
</Card>
<Form.Item name="knowledge_retrieval" noStyle>
<Knowledge />
</Form.Item>
{/* Memory Configuration */}
{/* Memory Configuration */}
<Card title={t('application.memoryConfiguration')}>
<Flex gap={16} vertical className="rb:bg-[#FAFAFA] rb:rounded-xl rb:p-3!">
<SwitchFormItem
@@ -403,6 +414,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
hasAll={false}
valueKey='config_id'
labelKey="config_name"
disabled={!values?.memory?.enabled}
/>
</Form.Item>
</Flex>
@@ -428,9 +440,6 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
title={t('application.debuggingAndPreview')}
extra={
<Space size={10}>
<Button type="primary" ghost onClick={handleOpenVariableConfig}>
{t('application.variableConfig')}
</Button>
<Button type="primary" ghost onClick={handleAddModel}>
+ {t('application.addModel')}
</Button>
@@ -441,14 +450,15 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
headerClassName="rb:h-[56px]! rb:leading-[22px]!"
titleClassName="rb:font-[MiSans-Bold] rb:font-bold"
bodyClassName="rb:p-4! rb:pt-0!"
className="rb:h-full"
className="rb:h-full!"
>
<Chat
data={data as Config}
data={values as Config}
chatList={chatList}
updateChatList={setChatList}
handleSave={handleSave}
chatVariables={chatVariables}
handleEditVariables={handleOpenVariableConfig}
/>
</RbCard>
</Col>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:33
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-07 17:11:54
* @Last Modified time: 2026-03-19 21:03:01
*/
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next'
@@ -18,7 +18,8 @@ import type {
ChatData,
SubAgentItem,
ClusterRef,
ModelConfigModalRef
ModelConfigModalRef,
FeaturesConfigForm
} from './types'
import Chat from './components/Chat'
import RbCard from '@/components/RbCard/Card'
@@ -28,13 +29,14 @@ import RadioGroupCard from '@/components/RadioGroupCard'
import ModelSelect from '@/components/ModelSelect'
import ModelConfigModal from './components/ModelConfigModal'
import type { Application } from '@/views/ApplicationManagement/types'
// import FeaturesConfig from './components/FeaturesConfig'
const MAX_LENGTH = 5;
/**
* Multi-agent cluster configuration component
* Manages multi-agent orchestration, sub-agents, and collaboration modes
*/
const Cluster = forwardRef<ClusterRef>((_props, ref) => {
const Cluster = forwardRef<ClusterRef, { onFeaturesLoad?: (features: FeaturesConfigForm | undefined) => void }>(({ onFeaturesLoad }, ref) => {
const { t } = useTranslation()
const { message } = App.useApp()
const [form] = Form.useForm()
@@ -129,6 +131,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
} else {
setSubAgents(sub_agents)
}
onFeaturesLoad?.(response.features)
})
.finally(() => {
setLoading(false)
@@ -167,7 +170,8 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
setSubAgents(prev => prev.filter(item => item.agent_id !== agent.agent_id))
}
useImperativeHandle(ref, () => ({
handleSave
handleSave,
features: data?.features
}))
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
@@ -186,6 +190,9 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
model_parameters: values
})
}
// const handleSaveFeaturesConfig = (value: FeaturesConfigForm) => {
// form.setFieldValue('features', value)
// }
return (
<>
@@ -195,10 +202,12 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
<Form form={form} layout="vertical">
<Flex gap={16} vertical>
<Flex align="center" justify="end" className="rb:p-3! rb:bg-white rb:rounded-xl">
{/* <FeaturesConfig value={values?.features as FeaturesConfigForm} refresh={handleSaveFeaturesConfig} /> */}
<Button type="primary" onClick={() => handleSave()}>
{t('common.save')}
</Button>
</Flex>
<Form.Item name="features" hidden noStyle></Form.Item>
<Card title={t('application.collaboration')}>
<Form.Item
name="orchestration_mode"
@@ -216,73 +225,73 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
</Form.Item>
</Card>
<Card
title={<>
{t('application.subAgentsManagement')}
<span className="rb:font-medium rb:font-[PingFangSC,PingFang_SC]! rb:text-[14px]!"> ({subAgents.length}/{MAX_LENGTH})</span>
</>}
extra={<Button className="rb:py-0! rb:px-2! rb:h-6!" disabled={subAgents.length >= MAX_LENGTH} onClick={() => handleSubAgentModal()}>+ {t('application.addSubAgent')}</Button>}
>
{subAgents.length === 0
? <div className="rb-border rb:rounded-xl rb:pt-4 rb:pb-6"><Empty size={88} /></div>
: <Flex vertical gap={12}>
{subAgents.map((agent, index) => (
<Flex key={index} align="center" justify="space-between"
className="rb:w-full! rb-border rb:rounded-xl rb:py-2.5! rb:pl-4! rb:pr-3!"
>
<Flex justify="center" vertical className="rb:max-w-[calc(100%-60px)]">
<div>
<span className="rb:text-[#212332] rb:leading-5">{agent.name}</span>
<Tag color={agent.is_active ? 'success' : 'warning'} className="rb:ml-2">
{agent.is_active ? t('common.enable') : t('common.deleted')}
</Tag>
</div>
{agent.role && <div className="rb:font-regular rb:leading-5 rb:text-[#5B6167] rb:text-[12px] rb:mt-1">{agent.role || '-'}</div>}
{agent.capabilities && <Flex wrap gap={8} className="rb:mt-2.5!">
{agent.capabilities.map((tag, tagIndex) => <Tag key={tagIndex} color="dark" className="rb:py-0!">{tag}</Tag>)}
</Flex>}
</Flex>
<Space>
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleSubAgentModal(agent)}
></div>
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteSubAgent(agent)}
></div>
</Space>
</Flex>
))}
</Flex>
}
</Card>
{values?.orchestration_mode !== 'collaboration' && <Card title={t('application.masterConfig')}>
<Form.Item
label={<span className="rb:text-[#5B6167]">{t('application.model')}</span>}
required={true}
className="rb:mb-4!"
<Card
title={<>
{t('application.subAgentsManagement')}
<span className="rb:font-medium rb:font-[PingFangSC,PingFang_SC]! rb:text-[14px]!"> ({subAgents.length}/{MAX_LENGTH})</span>
</>}
extra={<Button className="rb:py-0! rb:px-2! rb:h-6!" disabled={subAgents.length >= MAX_LENGTH} onClick={() => handleSubAgentModal()}>+ {t('application.addSubAgent')}</Button>}
>
<Flex align="center" gap={12}>
<Form.Item name="default_model_config_id" noStyle>
<ModelSelect
params={{ type: 'llm,chat' }}
className="rb:w-full!"
/>
</Form.Item>
<Form.Item name="model_parameters" noStyle>
<Button
className="rb:w-33"
icon={<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/application/set.svg')]"></div>}
onClick={handleEditModelConfig}
>{t('application.modelConfig')}</Button>
</Form.Item>
</Flex>
{subAgents.length === 0
? <div className="rb-border rb:rounded-xl rb:pt-4 rb:pb-6"><Empty size={88} /></div>
: <Flex vertical gap={12}>
{subAgents.map((agent, index) => (
<Flex key={index} align="center" justify="space-between"
className="rb:w-full! rb-border rb:rounded-xl rb:py-2.5! rb:pl-4! rb:pr-3!"
>
<Flex justify="center" vertical className="rb:max-w-[calc(100%-60px)]">
<div>
<span className="rb:text-[#212332] rb:leading-5">{agent.name}</span>
<Tag color={agent.is_active ? 'success' : 'warning'} className="rb:ml-2">
{agent.is_active ? t('common.enable') : t('common.deleted')}
</Tag>
</div>
{agent.role && <div className="rb:font-regular rb:leading-5 rb:text-[#5B6167] rb:text-[12px] rb:mt-1">{agent.role || '-'}</div>}
{agent.capabilities && <Flex wrap gap={8} className="rb:mt-2.5!">
{agent.capabilities.map((tag, tagIndex) => <Tag key={tagIndex} color="dark" className="rb:py-0!">{tag}</Tag>)}
</Flex>}
</Flex>
<Space>
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleSubAgentModal(agent)}
></div>
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteSubAgent(agent)}
></div>
</Space>
</Flex>
))}
</Flex>
}
</Card>
{values?.orchestration_mode !== 'collaboration' && <Card title={t('application.masterConfig')}>
<Form.Item
label={<span className="rb:text-[#5B6167]">{t('application.model')}</span>}
required={true}
className="rb:mb-4!"
>
<Flex align="center" gap={12}>
<Form.Item name="default_model_config_id" noStyle>
<ModelSelect
params={{ type: 'llm,chat' }}
className="rb:w-full!"
/>
</Form.Item>
<Form.Item name="model_parameters" noStyle>
<Button
className="rb:w-33"
icon={<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/application/set.svg')]"></div>}
onClick={handleEditModelConfig}
>{t('application.modelConfig')}</Button>
</Form.Item>
</Flex>
</Form.Item>
<Form.Item
name={['execution_config',"sub_agent_execution_mode"]}
name={['execution_config', "sub_agent_execution_mode"]}
label={<span className="rb:text-[#5B6167]">{t('application.orchestrationMode')}</span>}
className="rb:mb-4!"
>
@@ -291,6 +300,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
value: type,
label: t(`application.${type}`),
}))}
placeholder={t('common.pleaseSelect')}
/>
</Form.Item>
<Form.Item
@@ -303,6 +313,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
value: type,
label: t(`application.${type}`),
}))}
placeholder={t('common.pleaseSelect')}
/>
</Form.Item>
</Card>}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:41
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 19:02:43
* @Last Modified time: 2026-03-19 21:10:38
*/
import { type FC, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,10 +11,11 @@ import { Space, Input, Form, App, Flex } from 'antd';
import Tag, { type TagProps } from './components/Tag'
import RbCard from '@/components/RbCard/Card'
import { getReleaseList, rollbackRelease } from '@/api/application'
import { getReleaseList, rollbackRelease, appExport } from '@/api/application'
import ReleaseModal from './components/ReleaseModal'
import ReleaseShareModal from './components/ReleaseShareModal'
import type { Release, ReleaseModalRef, ReleaseShareModalRef } from './types'
import AppSharingModal from './components/AppSharingModal'
import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types'
import type { Application } from '@/views/ApplicationManagement/types'
import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format';
@@ -41,6 +42,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
const { message } = App.useApp()
const releaseModalRef = useRef<ReleaseModalRef>(null)
const releaseShareModalRef = useRef<ReleaseShareModalRef>(null)
const appSharingModalRef = useRef<AppSharingModalRef>(null)
const [selectedVersion, setSelectedVersion] = useState<Release | null>(null);
const [releaseList, setReleaseList] = useState<Release[]>([])
@@ -69,6 +71,10 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
message.success(t('common.operateSuccess'))
})
}
const handleExport = () => {
if (!selectedVersion) return
appExport(data.id, data.name, { release_id: selectedVersion.id})
}
return (
<Flex gap={12}>
<div className={`rb:overflow-y-auto rb:w-101 rb:flex-[0_0_auto] ${heightClass}`}>
@@ -125,9 +131,10 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
<Space size={10}>
{selectedVersion && <>
{/* <RbButton>{t('application.exportDSLFile')}</RbButton> */}
{data?.type !== 'multi_agent' && <RbButton onClick={handleExport}>{t('common.export')}</RbButton>}
{data.current_release_id !== selectedVersion.id && <RbButton onClick={handleRollback}>{t('application.willRollToThisVersion')}</RbButton>}
<RbButton onClick={() => releaseShareModalRef.current?.handleOpen()}>{t('application.share')}</RbButton>
<RbButton type="primary" ghost onClick={() => releaseShareModalRef.current?.handleOpen()}>{t('application.share')}</RbButton>
{data?.type !== 'multi_agent' && <RbButton type="primary" ghost onClick={() => appSharingModalRef.current?.handleOpen()}>{t('application.sharing')}</RbButton>}
</>}
<RbButton type="primary" onClick={() => releaseModalRef.current?.handleOpen()}>{t('application.release')}</RbButton>
</Space>
@@ -184,6 +191,11 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
ref={releaseShareModalRef}
version={selectedVersion}
/>
<AppSharingModal
ref={appSharingModalRef}
appId={data.id}
version={selectedVersion}
/>
</Flex>
);
}

View File

@@ -0,0 +1,485 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-13 17:27:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 20:54:35
*/
import { type FC, useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { App } from 'antd'
import clsx from 'clsx'
import dayjs from 'dayjs'
import ChatIcon from '@/assets/images/application/chat.png'
import { draftRun } from '@/api/application'
import Empty from '@/components/Empty'
import Chat from '@/components/Chat'
import RbCard from '@/components/RbCard/Card'
import ChatToolbar, { type ChatToolbarRef } from '@/components/Chat/ChatToolbar'
import Runtime from '@/views/Workflow/components/Chat/Runtime'
import { nodeLibrary } from '@/views/Workflow/constant'
import type { ChatItem } from '@/components/Chat/types'
import type { WorkflowConfig } from '@/views/Workflow/types'
import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types'
import type { TestChatProps } from './type'
import type { SSEMessage } from '@/utils/stream'
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
const formatParams = (message: string, conversation_id: string | null, files: any[] = [], variables: Record<string, any>) => {
return {
message,
conversation_id,
stream: true,
files: files.map(file => {
if (file.url) {
return file
} else {
return {
type: file.type,
transfer_method: 'local_file',
upload_file_id: file.response.data.file_id
}
}
}),
variables: Object.keys(variables).length > 0 ? variables : undefined
}
}
interface NodeData {
content: string;
conversation_id: string | null;
cycle_id: string;
cycle_idx: number;
node_id: string;
node_name?: string;
node_type?: string;
input?: any;
output?: any;
elapsed_time?: string;
error?: any;
state: Record<string, any>;
status?: 'completed' | 'failed';
audio_url?: string;
}
const TestChat: FC<TestChatProps> = ({
application,
config
}) => {
const { t } = useTranslation()
const { message: messageApi } = App.useApp()
const toolbarRef = useRef<ChatToolbarRef>(null)
const [loading, setLoading] = useState(false)
const [chatList, setChatList] = useState<ChatItem[]>([])
const [streamLoading, setStreamLoading] = useState(false)
const [conversationId, setConversationId] = useState<string | null>(null)
const [message, setMessage] = useState<string | undefined>(undefined)
const [fileList, setFileList] = useState<any[]>([])
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
useEffect(() => {
getVariables()
}, [application, config])
const getVariables = () => {
if (!application || !config) return
setFeatures(config?.features || {} as FeaturesConfigForm)
let initVariables: Variable[] = []
switch (application.type) {
case 'workflow':
const { nodes } = config as WorkflowConfig;
const startNodes = nodes.filter(vo => vo.type === 'start')
if (startNodes.length) {
const curVariables = startNodes[0].config.variables as Variable[]
curVariables.forEach((vo) => {
if (typeof vo.default !== 'undefined') {
vo.value = vo.default
}
const lastVo = curVariables.find(item => item.name === vo.name)
if (lastVo?.value) {
vo.value = lastVo.value
}
})
initVariables = curVariables
}
break
case 'agent':
initVariables = config.variables as Variable[]
break
}
toolbarRef.current?.setVariables([...initVariables])
}
const addUserMessage = (message: string, files: any[]) => {
setChatList(prev => [...prev, {
role: 'user',
content: message,
created_at: Date.now(),
meta_data: {
files
},
}])
}
const addAssistantMessage = () => {
const { type } = application || {}
setChatList(prev => [...prev, {
role: 'assistant',
content: '',
created_at: Date.now(),
subContent: type === 'workflow' ? [] : undefined,
}])
}
const updateAssistantMessage = (content: string, audio_url?: string) => {
setChatList(prev => {
const newList = [...prev]
const lastMsg = newList[newList.length - 1]
if (lastMsg.role === 'assistant') {
lastMsg.content += content;
lastMsg.meta_data = {audio_url}
}
return newList
})
}
const updateErrorAssistantMessage = (message_length: number) => {
if (message_length > 0) return
setChatList(prev => {
const newList = [...prev]
const lastMsg = newList[newList.length - 1]
if (lastMsg.role === 'assistant') {
lastMsg.content = null
}
return newList
})
}
const buildVariableParams = (variables: Variable[]) => {
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')}`)
}
}
return { isCanSend, params }
}
const handleSend = () => {
if (loading || !application || !message || !message?.trim()) return
const files = toolbarRef.current?.getFiles() || []
const variables = toolbarRef.current?.getVariables() || []
const { isCanSend, params } = buildVariableParams(variables)
if (!isCanSend) return
addUserMessage(message, files)
setMessage(undefined)
toolbarRef.current?.setFiles([])
setFileList([])
addAssistantMessage()
setStreamLoading(true)
setLoading(true)
draftRun(
application.id,
formatParams(message, conversationId, files, params),
handleStreamMessage
)
.catch(() => {
updateErrorAssistantMessage(0)
setLoading(false)
})
.finally(() => {
setLoading(false)
setStreamLoading(false)
})
}
const handleStreamMessage = (data: SSEMessage[]) => {
data.map(item => {
const { conversation_id, content, message_length, audio_url } = item.data as { conversation_id: string, content: string, message_length: number; audio_url?: string; };
switch (item.event) {
case 'start':
if (conversation_id && conversationId !== conversation_id) setConversationId(conversation_id)
break
case 'message':
updateAssistantMessage(content)
if (conversation_id && conversationId !== conversation_id) setConversationId(conversation_id)
break
case 'end':
if (audio_url) {
updateAssistantMessage(content, audio_url)
}
updateErrorAssistantMessage(message_length)
setStreamLoading(false)
break
}
})
}
const handleWorkflowSend = () => {
if (loading || !application || !message || !message?.trim()) return
const files = toolbarRef.current?.getFiles() || []
const variables = toolbarRef.current?.getVariables() || []
const { isCanSend, params } = buildVariableParams(variables)
if (!isCanSend) return
setLoading(true)
addUserMessage(message, files)
addAssistantMessage()
toolbarRef.current?.setFiles([])
setFileList([])
setMessage(undefined)
setStreamLoading(true)
draftRun(
application.id,
formatParams(message, conversationId, files, params),
handleWorkflowStreamMessage
)
.catch((error) => {
const errorInfo = JSON.parse(error.message)
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
newList[lastIndex] = { ...newList[lastIndex], status: 'failed', content: null, subContent: errorInfo.error }
}
return newList
})
})
.finally(() => {
setLoading(false)
setStreamLoading(false)
})
}
const handleWorkflowStreamMessage = (data: SSEMessage[]) => {
data.forEach(item => {
const { content, conversation_id } = item.data as NodeData;
switch (item.event) {
// Append streaming text chunks to assistant message
case 'message':
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
newList[lastIndex] = { ...newList[lastIndex], content: newList[lastIndex].content + content }
}
return newList
})
break
// Track node execution start
case 'node_start':
addWorkflowNodeStartMessage(item.data as NodeData)
break
// Update node with execution results or errors
case 'node_end':
case 'node_error':
updateWorkflowNodeEndMessage(item.data as NodeData)
break
// Update node with subContent
case 'cycle_item':
updateWorkflowCycleMessage(item.data as NodeData)
break
// Mark workflow as complete
case 'workflow_end':
updateWorkflowEndMessage(item.data as NodeData)
setStreamLoading(false)
setLoading(false)
break
}
if (conversation_id && conversationId !== conversation_id) {
setConversationId(conversation_id)
}
})
}
const addWorkflowNodeStartMessage = (data: NodeData) => {
const { node_id } = data;
const { nodes } = config as WorkflowConfig
const node = nodes.find(n => n.id === node_id);
const { name, type } = node || {}
const icon = nodeLibrary.flatMap(g => g.nodes).find(n => n.type === type)?.icon
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
const newSubContent = newList[lastIndex].subContent || []
const filterIndex = newSubContent.findIndex(vo => vo.id === node_id)
if (filterIndex > -1) {
newSubContent[filterIndex] = {
...newSubContent[filterIndex],
node_id: node_id,
node_name: name,
node_type: type,
icon,
content: {},
}
} else {
newSubContent.push({
id: node_id,
node_id: node_id,
node_name: name,
node_type: type,
icon,
content: {},
})
}
newList[lastIndex] = {
...newList[lastIndex],
subContent: newSubContent
}
}
return newList
})
}
const updateWorkflowNodeEndMessage = (data: NodeData) => {
const { node_id, input, output, error, elapsed_time, status } = data;
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
const newSubContent = newList[lastIndex].subContent || []
const filterIndex = newSubContent.findIndex(vo => vo.node_id === node_id)
if (filterIndex > -1 && newSubContent[filterIndex].content) {
newSubContent[filterIndex] = {
...newSubContent[filterIndex],
content: {
input,
output,
error,
},
status: status || 'completed',
elapsed_time
}
}
newList[lastIndex] = {
...newList[lastIndex],
subContent: newSubContent
}
}
return newList
})
}
const updateWorkflowCycleMessage = (data: NodeData) => {
const { node_id, cycle_id, cycle_idx, input, output, error, elapsed_time, status } = data;
const { nodes } = config as WorkflowConfig
const node = nodes.find(n => n.id === node_id);
const { name, type } = node || {}
const icon = nodeLibrary.flatMap(g => g.nodes).find(n => n.type === type)?.icon
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
const newSubContent = newList[lastIndex].subContent || []
const filterIndex = newSubContent.findIndex(vo => vo.id === cycle_id)
if (filterIndex > -1) {
const items = newSubContent[filterIndex].subContent || []
items.push({
cycle_id,
cycle_idx,
node_id,
node_name: name,
node_type: type,
icon,
content: {
cycle_idx,
input,
output,
error,
},
status: status || 'completed',
elapsed_time
})
newSubContent[filterIndex] = {
...newSubContent[filterIndex],
subContent: [...items]
}
newList[lastIndex] = {
...newList[lastIndex],
subContent: newSubContent
}
}
}
return newList
})
}
const updateWorkflowEndMessage = (data: NodeData) => {
const { error, status, audio_url } = data;
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
newList[lastIndex] = {
...newList[lastIndex],
status,
error,
content: newList[lastIndex].content === '' ? null : newList[lastIndex].content,
meta_data: { audio_url: audio_url }
}
}
return newList
})
}
return (
<div className="rb:w-250 rb:p-3 rb:mx-auto">
<RbCard
title={t('application.test')}
headerClassName="rb:min-h-[56px]!"
className="rb:h-[calc(100vh-88px)]!"
bodyClassName="rb:h-[calc(100%-56px)]! rb:overflow-y-auto rb:px-3! rb:py-0!"
>
<Chat
empty={<Empty url={ChatIcon} title={t('application.testChatEmpty')} isNeedSubTitle={false} size={[240, 200]} />}
contentClassName={clsx(`rb:mx-[16px] rb:pt-[24px]`, {
'rb:h-[calc(100%-140px)]': !fileList.length,
'rb:h-[calc(100%-208px)]': !!fileList.length,
})}
data={chatList}
streamLoading={streamLoading}
loading={loading}
onChange={setMessage}
onSend={application?.type === 'workflow' ? handleWorkflowSend : handleSend}
fileList={fileList}
fileChange={(list) => {
setFileList(list || [])
toolbarRef.current?.setFiles(list || [])
}}
labelFormat={(item) => item.role === 'user' ? t('application.you') : dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
errorDesc={t('application.ReplyException')}
renderRuntime={application?.type === 'workflow' ? (item, index) => <Runtime item={item} index={index} /> : undefined}
>
<ChatToolbar
ref={toolbarRef}
features={features}
onFilesChange={setFileList}
/>
</Chat>
</RbCard>
</div>
)
}
export default TestChat

View File

@@ -0,0 +1,8 @@
import type { Application } from '@/views/ApplicationManagement/types'
import type { Config } from '../types';
import type { WorkflowConfig } from '@/views/Workflow/types';
export interface TestChatProps {
application?: Application | null;
config: Config | WorkflowConfig | null
}

View File

@@ -0,0 +1,189 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-13 17:19:13
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 16:03:46
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Checkbox, App, Form } from 'antd';
import { useTranslation } from 'react-i18next';
import RbModal from '@/components/RbModal';
import { appSharing, getAppShares } from '@/api/application';
import { formatDateTime } from '@/utils/format';
import type { AppSharingModalRef, Release } from '../types';
import type { SpaceItem } from '@/views/KnowledgeBase/types';
import { getWorkspaces } from '@/api/workspaces';
import RadioGroupCard from '@/components/RadioGroupCard';
/** Props for the AppSharingModal component */
interface AppSharingModalProps {
/** ID of the application being shared */
appId: string;
/** The release version to share */
version: Release | null;
}
const AppSharingModal = forwardRef<AppSharingModalRef, AppSharingModalProps>(({ appId, version }, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
// All workspaces available to share with (excluding the current one)
const [spaceList, setSpaceList] = useState<SpaceItem[]>([]);
// IDs of workspaces that already have access to this app
const [sharedIds, setSharedIds] = useState<string[]>([]);
const [form] = Form.useForm<{ target_workspace_ids: string[]; permission: 'readonly' | 'editable' }>();
// Reactively track the currently selected workspace IDs in the form
const selectedIds: string[] = Form.useWatch('target_workspace_ids', form) ?? [];
/**
* Fetch workspaces and existing share records in parallel,
* sort already-shared spaces to the top, then open the modal.
* Shows a warning if the user has no shareable workspaces.
*/
const handleOpen = () => {
Promise.all([getWorkspaces({ include_current: false }), getAppShares(appId)]).then(([spaces, shared]) => {
// Normalise the shared workspace ID field across different API response shapes
const ids = ((shared as any[]) || []).map((s: any) => s.workspace_id || s.target_workspace_id || s.id);
// Sort: already-shared workspaces appear first
const sorted = (spaces as SpaceItem[]).sort((a, b) =>
ids.includes(b.id) ? 1 : ids.includes(a.id) ? -1 : 0
);
setSpaceList(sorted);
setSharedIds(ids);
if (sorted.length > 0) {
setVisible(true);
} else {
message.warning(t('application.noShareAuth'));
}
});
};
/** Close the modal and reset form fields */
const handleClose = () => {
setVisible(false);
form.resetFields();
};
// Expose open/close handlers to the parent via ref
useImperativeHandle(ref, () => ({ handleOpen, handleClose }));
/**
* Toggle a workspace in the selected list.
* Already-shared workspaces are read-only and cannot be toggled.
*/
const handleToggle = (id: string, isShared: boolean) => {
if (isShared) return;
const prev: string[] = form.getFieldValue('target_workspace_ids') ?? [];
form.setFieldValue(
'target_workspace_ids',
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
/** Validate the form then submit the sharing request */
const handleConfirm = () => {
form.validateFields().then(values => {
setLoading(true);
appSharing(appId, values)
.then(() => {
message.success(t('common.operateSuccess'));
handleClose();
})
.finally(() => setLoading(false));
});
};
// Normalise the version label to always start with "v"
const versionLabel = version?.version_name
? (version.version_name[0].toLowerCase() === 'v' ? version.version_name : `v${version.version_name}`)
: `v${version?.version}`;
return (
<RbModal
title={t('application.sharingApp')}
open={visible}
onCancel={handleClose}
okText={<>{t('application.confirmSharing')}({selectedIds.length})</>}
onOk={handleConfirm}
confirmLoading={loading}
width={600}
>
<Form form={form} layout="vertical" initialValues={{ target_workspace_ids: [], permission: 'readonly' }}>
{/* Version info: displays version number, release time and publisher */}
<div className="rb:rounded-lg rb:border rb:border-[#EBEBEB] rb:bg-[#FBFDFF] rb:p-4 rb:mb-4">
<div className="rb:text-sm rb:font-medium rb:mb-3">{t('application.VersionInformation')}</div>
<div className="rb:grid rb:grid-cols-3 rb:gap-4 rb:text-sm">
<div>
<div className="rb:text-[#5B6167] rb:mb-1">{t('application.versionList').replace('列表', '号')}</div>
<div className="rb:font-medium">{versionLabel}</div>
</div>
<div>
<div className="rb:text-[#5B6167] rb:mb-1">{t('application.releaseTime')}</div>
<div className="rb:font-medium">{formatDateTime(version?.published_at || 0, 'YYYY-MM-DD HH:mm:ss')}</div>
</div>
<div>
<div className="rb:text-[#5B6167] rb:mb-1">{t('application.publisher')}</div>
<div className="rb:font-medium">{version?.publisher_name}</div>
</div>
</div>
</div>
{/* Target space: scrollable list of workspaces with checkbox selection */}
<Form.Item
label={t('application.selectTargetSpace')}
required
>
<Form.Item
name="target_workspace_ids"
noStyle
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<input type="hidden" />
</Form.Item>
<div className="rb:rounded-lg rb:border rb:border-[#EBEBEB] rb:divide-y rb:divide-[#EBEBEB] rb:max-h-50 rb:overflow-y-auto">
{spaceList.map(space => {
const isShared = sharedIds.includes(space.id);
return (
<div key={space.id} className="rb:flex rb:items-center rb:gap-2 rb:px-4 rb:py-3 rb:cursor-pointer" onClick={() => handleToggle(space.id, isShared)}>
<Checkbox
checked={isShared || selectedIds.includes(space.id)}
disabled={isShared}
onClick={(e) => e.stopPropagation()}
onChange={() => handleToggle(space.id, isShared)}
/>
<span className="rb:flex-1 rb:text-sm">{space.name}</span>
{isShared && (
<span className="rb:text-xs rb:text-[#5B6167]">{t('application.alreadyShared')}</span>
)}
</div>
);
})}
</div>
</Form.Item>
{/* Permission mode: readonly (use only) or editable (full copy) */}
<Form.Item
name="permission"
label={t('application.permissionMode')}
rules={[{ required: true, message: t('common.pleaseSelect') }]}
className="rb:mb-0!"
>
<RadioGroupCard
options={['readonly', 'editable'].map((type) => ({
value: type,
label: t(`application.${type}Mode`),
labelDesc: t(`application.${type}ModeDesc`),
}))}
/>
</Form.Item>
</Form>
</RbModal>
);
});
export default AppSharingModal;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:39
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-05 17:03:46
* @Last Modified time: 2026-03-20 11:38:45
*/
/**
* Chat debugging component for application testing
@@ -12,24 +12,25 @@
import { type FC, useEffect, useState, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom'
import clsx from 'clsx'
import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd'
import { App, Flex } from 'antd';
import { SettingOutlined } from '@ant-design/icons'
import ChatIcon from '@/assets/images/application/chat.png'
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
import type { ChatData, Config } from '../types'
import type { ChatData, Config, FeaturesConfigForm } from '../types'
import { runCompare, draftRun } from '@/api/application'
import Empty from '@/components/Empty'
import ChatContent from '@/components/Chat/ChatContent'
import type { ChatItem } from '@/components/Chat/types'
import { type SSEMessage } from '@/utils/stream'
import ChatInput from '@/components/Chat/ChatInput'
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 ChatToolbar from '@/components/Chat/ChatToolbar'
import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
import type { Variable } from './VariableList/types'
/**
* Component props
*/
@@ -44,27 +45,44 @@ interface ChatProps {
handleSave: (flag?: boolean) => Promise<unknown>;
/** Source type: multi-agent cluster or single agent */
source?: 'multi_agent' | 'agent';
chatVariables?: Variable[]; // Add chatVariables prop
/** chatVariables prop */
chatVariables?: Variable[];
handleEditVariables?: () => void;
}
/**
* Chat debugging component
* Allows testing application with different model configurations side-by-side
*/
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent', chatVariables }) => {
const Chat: FC<ChatProps> = ({
chatList, data, updateChatList, handleSave, source = 'agent', chatVariables,
handleEditVariables
}) => {
const { t } = useTranslation();
const { id } = useParams()
const { message: messageApi } = App.useApp()
const toolbarRef = useRef<ChatToolbarRef>(null)
const [loading, setLoading] = useState(false)
const [isCluster, setIsCluster] = useState(source === 'multi_agent')
const [conversationId, setConversationId] = useState<string | null>(null)
const [compareLoading, setCompareLoading] = useState(false)
const [fileList, setFileList] = useState<any[]>([])
const [message, setMessage] = useState<string | undefined>(undefined)
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
useEffect(() => {
setCompareLoading(false)
setLoading(false)
}, [chatList.map(item => item.label).join(',')])
useEffect(() => {
if (data?.features) setFeatures(data.features)
}, [data?.features])
useEffect(() => {
setIsCluster(source === 'multi_agent')
setFileList([])
toolbarRef.current?.setFiles([])
setMessage(undefined)
}, [source])
@@ -74,7 +92,9 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
role: 'user',
content: message,
created_at: Date.now(),
files
meta_data: {
files
},
};
updateChatList(prev => prev.map(item => ({
...item,
@@ -106,8 +126,8 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
}
}
/** Update assistant message with streaming content */
const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string) => {
if (!content || !model_config_id) return
const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string, audio_url?: string) => {
if ((!content && !audio_url) || !model_config_id) return
updateChatList(prev => {
const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id);
if (targetIndex !== -1) {
@@ -118,12 +138,13 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
if (lastMsg && lastMsg.role === 'assistant') {
modelChatList[targetIndex] = {
...modelChatList[targetIndex],
conversation_id: conversation_id,
conversation_id,
list: [
...curChatMsgList.slice(0, curChatMsgList.length - 1),
{
...lastMsg,
content: lastMsg.content + content
content: lastMsg.content + (content || ''),
meta_data: { audio_url }
}
]
}
@@ -162,13 +183,14 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
}
/** Send message for agent comparison mode */
const handleSend = (msg?: string) => {
if (loading) return
if (loading || !id) return
setLoading(true)
setCompareLoading(true)
handleSave(false)
.then(() => {
const message = msg
if (!message?.trim()) return
const files = toolbarRef.current?.getFiles() || []
// Validate required variables before sending
let isCanSend = true
const params: Record<string, any> = {}
@@ -193,8 +215,9 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
return
}
addUserMessage(message, fileList)
addUserMessage(message, files)
setMessage(message)
toolbarRef.current?.setFiles([])
setFileList([])
addAssistantMessage()
@@ -202,13 +225,16 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
setCompareLoading(false)
data.map(item => {
const { model_config_id, conversation_id, content, message_length } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number };
const { model_config_id, conversation_id, content, message_length, audio_url } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number; audio_url: string };
switch (item.event) {
case 'model_message':
updateAssistantMessage(content, model_config_id, conversation_id)
updateAssistantMessage(content, model_config_id, conversation_id, audio_url)
break;
case 'model_end':
if (audio_url) {
updateAssistantMessage(content, model_config_id, conversation_id, audio_url)
}
updateErrorAssistantMessage(message_length, model_config_id)
break;
case 'compare_end':
@@ -219,9 +245,9 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
};
setTimeout(() => {
runCompare(data.app_id, {
runCompare(id, {
message,
files: fileList.map(file => {
files: files.map(file => {
if (file.url) {
return file
} else {
@@ -239,9 +265,9 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
conversation_id: item.conversation_id
})),
variables: params,
"parallel": true,
"stream": true,
"timeout": 60,
parallel: true,
stream: true,
timeout: 60,
}, handleStreamMessage)
.catch(() => {
setLoading(false)
@@ -265,7 +291,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
const assistantMessage: ChatItem = {
role: 'assistant',
content: '',
created_at: Date.now(),
created_at: Date.now()
};
updateChatList(prev => prev.map(item => ({
...item,
@@ -277,8 +303,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
if (!content) return
updateChatList(prev => {
const modelChatList = [...prev]
const curModelChat = modelChatList[0]
const curChatMsgList = curModelChat.list || []
const curChatMsgList = modelChatList[0].list || []
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
if (lastMsg.role === 'assistant') {
modelChatList[0] = {
@@ -298,11 +323,9 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
/** Update cluster message when error occurs */
const updateClusterErrorAssistantMessage = (message_length: number) => {
if (message_length > 0) return
updateChatList(prev => {
const modelChatList = [...prev]
const curModelChat = modelChatList[0]
const curChatMsgList = curModelChat.list || []
const curChatMsgList = modelChatList[0].list || []
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
if (lastMsg.role === 'assistant') {
modelChatList[0] = {
@@ -321,15 +344,17 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
}
/** Send message for cluster mode */
const handleClusterSend = (msg?: string) => {
if (loading) return
if (loading || !id) return
setLoading(true)
setCompareLoading(true)
handleSave(false)
.then(() => {
const message = msg
if (!message || message.trim() === '') return
addUserMessage(message, fileList)
const files = toolbarRef.current?.getFiles() || []
addUserMessage(message, files)
setMessage(undefined)
toolbarRef.current?.setFiles([])
setFileList([])
addClusterAssistantMessage()
@@ -338,7 +363,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
data.map(item => {
const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number };
switch (item.event) {
case 'start':
if (conversation_id && conversationId !== conversation_id) {
@@ -362,13 +387,12 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
};
setTimeout(() => {
draftRun(
data.app_id,
draftRun(id,
{
message,
conversation_id: conversationId,
stream: true,
files: fileList.map(file => {
files: files.map(file => {
if (file.url) {
return file
} else {
@@ -403,35 +427,6 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
const handleDelete = (index: number) => {
updateChatList(chatList.filter((_, voIndex) => voIndex !== index))
}
const handleMessageChange = (message: string) => {
setMessage(message)
}
const fileChange = (file?: any) => {
setFileList([...fileList, file])
}
const handleRecordingComplete = async (file: any) => {
setFileList([...fileList, {
uid: file.file_id,
response: { data: file },
thumbUrl: file.url,
type: file.type
}])
}
const handleShowUpload: MenuProps['onClick'] = ({ key }) => {
switch (key) {
case 'define':
uploadFileListModalRef.current?.handleOpen()
break
}
}
const addFileList = (list?: any[]) => {
if (!list || list.length <= 0) return
setFileList([...fileList, ...(list || [])])
}
const updateFileList = (list?: any[]) => {
setFileList([...list || []])
}
const isHasLabel = useMemo(() => chatList.some(item => item.label), [chatList])
return (
@@ -444,105 +439,95 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
subTitle={t('application.debuggingEmptyDesc')}
className="rb:h-[calc(100vh-159px)]"
/>
: <>
<div className={clsx(`rb:relative rb:grid rb:grid-cols-${chatList.length} rb:overflow-hidden rb:w-full rb:flex-1 rb:min-h-0`)}>
{chatList.map((chat, index) => (
<Flex key={index} vertical className={clsx({
"rb:border-r rb:border-[#DFE4ED]": index !== chatList.length - 1 && chatList.length > 1,
})}>
{chat.label &&
<div className={clsx(
"rb:grid rb:bg-[#F6F6F6] rb:text-center rb:flex-[0_0_auto]"
)}>
<div className='rb:relative rb:py-2.5 rb:px-3 rb:overflow-hidden'>
<div className="rb:text-[#212332] rb:font-medium rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:w-[calc(100%-24px)]">{chat.label}</div>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:absolute rb:top-3 rb:right-3 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
onClick={() => handleDelete(index)}
></div>
: <>
<div className={clsx(`rb:relative rb:grid rb:grid-cols-${chatList.length} rb:overflow-hidden rb:w-full rb:flex-1 rb:min-h-0`)}>
{chatList.map((chat, index) => (
<Flex key={index} vertical className={clsx({
"rb:border-r rb:border-[#DFE4ED]": index !== chatList.length - 1 && chatList.length > 1,
})}>
{chat.label &&
<div className={clsx(
"rb:grid rb:bg-[#F6F6F6] rb:text-center rb:flex-[0_0_auto]"
)}>
<div className='rb:relative rb:py-2.5 rb:px-3 rb:overflow-hidden'>
<div className="rb:text-[#212332] rb:font-medium rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:w-[calc(100%-24px)]">{chat.label}</div>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:absolute rb:top-3 rb:right-3 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
onClick={() => handleDelete(index)}
></div>
</div>
</div>
</div>
}
<ChatContent
classNames={{
'rb:mb-3 rb:mt-5': isHasLabel,
'rb:mb-3': !isHasLabel,
'rb:h-[calc(100vh-292px)]': isCluster,
'rb:h-[calc(100vh-353px)]': !isCluster,
"rb:pr-4": index !== chatList.length - 1 && chatList.length > 1,
"rb:pl-4": index !== 0 && chatList.length > 1,
}}
contentClassNames={{
'rb:max-w-100!': chatList.length === 1,
'rb:max-w-70!': chatList.length === 2,
'rb:max-w-45!': chatList.length === 3,
'rb:max-w-24!': chatList.length === 4,
}}
empty={<Empty
url={ChatIcon}
title={t('application.chatEmpty')}
isNeedSubTitle={false}
size={[240, 200]}
className={clsx({
"rb:h-[calc(100vh-353px)]": isHasLabel,
"rb:h-[calc(100vh-292px)]": !isHasLabel,
})}
/>}
data={chat.list || []}
streamLoading={compareLoading}
labelPosition="top"
labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label || t(`application.ai`)}
errorDesc={t('application.ReplyException')}
/>
</Flex>
))}
</div>
<div className="rb:relative rb:flex rb:items-center rb:gap-2.5 rb:m-4 rb:mb-1">
}
<ChatContent
classNames={{
'rb:mb-3 rb:mt-5': isHasLabel,
'rb:mb-3': !isHasLabel,
'rb:h-[calc(100vh-292px)]': isCluster,
'rb:h-[calc(100vh-353px)]': !isCluster,
"rb:pr-4": index !== chatList.length - 1 && chatList.length > 1,
"rb:pl-4": index !== 0 && chatList.length > 1,
}}
contentClassNames={{
'rb:max-w-100!': chatList.length === 1,
'rb:max-w-70!': chatList.length === 2,
'rb:max-w-45!': chatList.length === 3,
'rb:max-w-24!': chatList.length === 4,
}}
empty={<Empty
url={ChatIcon}
title={t('application.chatEmpty')}
isNeedSubTitle={false}
size={[240, 200]}
className={clsx({
"rb:h-[calc(100vh-353px)]": isHasLabel,
"rb:h-[calc(100vh-292px)]": !isHasLabel,
})}
/>}
data={chat.list || []}
streamLoading={compareLoading}
labelPosition="top"
labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label || t(`application.ai`)}
errorDesc={t('application.ReplyException')}
/>
</Flex>
))}
</div>
<div className="rb:relative rb:flex rb:items-center rb:gap-2.5 rb:mt-4 rb:mb-1">
<ChatInput
message={message}
className="rb:relative!"
loading={loading}
fileChange={updateFileList}
fileChange={(list) => {
setFileList(list || [])
toolbarRef.current?.setFiles(list || [])
}}
fileList={fileList}
onSend={isCluster ? handleClusterSend : handleSend}
onChange={handleMessageChange}
onChange={setMessage}
>
<Flex justify="space-between" className="rb:flex-1">
<Flex gap={8} align="center">
<Dropdown
menu={{
items: [
{ key: 'define', label: t('memoryConversation.addRemoteFile') },
{
key: 'upload', label: (
<UploadFiles
onChange={fileChange}
/>
)
},
],
onClick: handleShowUpload
}}
>
<ChatToolbar
ref={toolbarRef}
features={features}
onFilesChange={setFileList}
extra={
chatVariables && chatVariables.length > 0 ? (
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')] rb:hover:bg-[url('@/assets/images/conversation/link_hover.svg')]"
></div>
</Dropdown>
</Flex>
<Flex align="center">
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
<Divider type="vertical" className="rb:ml-1.5! rb:mr-3!" />
</Flex>
</Flex>
</ChatInput>
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]': chatVariables.some(vo => vo.required && !vo.value),
'rb:border-[#DFE4ED]': !chatVariables.some(vo => vo.required && !vo.value),
})}
onClick={handleEditVariables}
>
<SettingOutlined className="rb:mr-1" />
{t('memoryConversation.variableConfig')}
</div>
) : null
}
/>
</ChatInput>
</div>
</>
</>
}
<UploadFileListModal
ref={uploadFileListModalRef}
refresh={addFileList}
/>
</Flex>
)
}

View File

@@ -2,9 +2,9 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 10:31:08
* @Last Modified time: 2026-03-19 21:21:28
*/
import { type FC, useRef, useMemo } from 'react';
import { type FC, useRef, useMemo, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Tabs, Dropdown, Button, Flex } from 'antd';
import type { MenuProps } from 'antd';
@@ -18,16 +18,21 @@ import exportIcon from '@/assets/images/export_hover.svg'
import deleteIcon from '@/assets/images/delete_hover.svg'
import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types';
import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal'
import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef } from '../types'
import { deleteApplication } from '@/api/application'
import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef, FeaturesConfigForm } from '../types'
import { deleteApplication, appExport } from '@/api/application'
import CopyModal from './CopyModal'
import PageHeader from '@/components/Layout/PageHeader'
import { exportToYaml } from '@/utils/yamlExport';
import FeaturesConfig from './FeaturesConfig'
/**
* Tab keys for application configuration
*/
const tabKeys = ['arrangement', 'api', 'release', 'statistics']
const sharingTabKeys = [
'test',
// 'log',
'api'
]
/**
* Menu icon mapping
@@ -55,6 +60,10 @@ interface ConfigHeaderProps {
workflowRef: React.RefObject<WorkflowRef>
/** App component ref (Agent/Cluster/Workflow) */
appRef?: React.RefObject<AgentRef | ClusterRef | WorkflowRef>
/** Features config from parent state */
features?: FeaturesConfigForm;
/** Callback to update features in parent */
onFeaturesChange?: (value: FeaturesConfigForm) => void;
}
/**
@@ -64,35 +73,45 @@ interface ConfigHeaderProps {
const ConfigHeader: FC<ConfigHeaderProps> = ({
application, activeTab, handleChangeTab, refresh,
workflowRef,
appRef,
features,
onFeaturesChange,
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { id } = useParams();
const { id, source } = useParams();
const applicationModalRef = useRef<ApplicationModalRef>(null);
const copyModalRef = useRef<CopyModalRef>(null);
/**
* Format tab items for display
*/
const formatTabItems = () => {
return tabKeys.map(key => ({
const formatTabItems = useMemo(() => {
return (source === 'sharing' ? sharingTabKeys : tabKeys).map(key => ({
key,
label: t(`application.${key}`),
}))
}
}, [source, sharingTabKeys, tabKeys])
/**
* Handle menu item click
*/
const handleClick: MenuProps['onClick'] = ({ key }) => {
if (!application) return
switch (key) {
case 'edit':
applicationModalRef.current?.handleOpen(application as Application)
applicationModalRef.current?.handleOpen(application)
break;
case 'copy':
copyModalRef.current?.handleOpen()
appRef?.current?.handleSave(false)
.then(() => {
copyModalRef.current?.handleOpen()
})
break;
case 'export':
exportToYaml(workflowRef?.current?.config, application?.name ? `${application?.name}.yml` : undefined)
appRef?.current?.handleSave(false)
.then(() => {
appExport(application.id, application.name)
})
break;
case 'delete':
handleDelete()
@@ -151,7 +170,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
* Format dropdown menu items
*/
const formatMenuItems = useMemo(() => {
const items = (application?.type === 'workflow' ? ['edit', 'copy', 'export', 'delete'] : ['edit', 'copy', 'delete']).map(key => ({
const items = (application?.type !== 'multi_agent' ? ['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}`),
@@ -159,7 +178,11 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
return items
}, [t, handleClick, application])
console.log('formatMenuItems', formatMenuItems)
const handleSaveFeaturesConfig = useCallback((value: FeaturesConfigForm) => {
appRef?.current?.handleSaveFeaturesConfig?.(value)
onFeaturesChange?.(value)
}, [appRef, onFeaturesChange])
return (
<>
<PageHeader
@@ -170,8 +193,8 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
'rb:bg-[#171719]': application?.type === 'workflow',
})}
title={application?.name || ''}
operation={<Dropdown
menu={{ items: formatMenuItems, onClick: handleClick }}
operation={source !== 'sharing' && <Dropdown
menu={{ items: formatMenuItems, onClick: handleClick }}
trigger={['click']}
placement="bottomRight"
>
@@ -182,13 +205,14 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
centerContent={<Flex justify="center" className="rb:h-16!">
<Tabs
activeKey={activeTab}
items={formatTabItems()}
items={formatTabItems}
onChange={handleChangeTab}
className={styles.tabs}
/>
</Flex>}
extra={application?.type === 'workflow'
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
<FeaturesConfig source={application?.type} value={features as FeaturesConfigForm} refresh={handleSaveFeaturesConfig} />
<Button onClick={clear}>{t('workflow.clear')}</Button>
<Button onClick={addvariable}>{t('workflow.addvariable')}</Button>
<Button onClick={run}>{t('workflow.run')}</Button>

View File

@@ -0,0 +1,156 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 15:38:14
*/
/**
* Copy Application Modal
* Allows users to duplicate an existing application with a new name
*/
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
import { Form, Button, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx'
import type { FeaturesConfigModalRef, FeaturesConfigForm } from '../../types'
import RbModal from '@/components/RbModal'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
import FileUploadSettingModal from './FileUploadSettingModal'
import type { Application } from '@/views/ApplicationManagement/types';
interface FeaturesConfigModalProps {
refresh: (value: FeaturesConfigForm) => void;
source?: Application['type'];
}
/**
* Modal for copying applications
*/
const FeaturesConfigModal = forwardRef<FeaturesConfigModalRef, FeaturesConfigModalProps>(({
refresh,
source,
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<FeaturesConfigForm>();
const values = Form.useWatch([], form)
const fileUploadSettingModalRef = useRef<any>(null)
/** Close modal and reset form */
const handleClose = () => {
setVisible(false);
form.resetFields();
};
/** Open modal */
const handleOpen = (initValue: FeaturesConfigForm) => {
setVisible(true);
console.log('initValue', initValue)
form.setFieldsValue(initValue)
};
/** Copy application with new name */
const handleSave = () => {
setVisible(false);
refresh(form.getFieldsValue())
}
const handleOpenSettings = () => {
fileUploadSettingModalRef.current?.handleOpen(values?.file_upload)
}
const handleSaveSettings = (settings: FeaturesConfigForm['file_upload']) => {
form.setFieldValue('file_upload', { ...settings, enabled: values?.file_upload?.enabled ?? false })
}
/** Expose methods to parent component */
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<>
<RbModal
title={t('application.features')}
open={visible}
onCancel={handleClose}
okText={t('common.confirm')}
onOk={handleSave}
>
<Form
form={form}
layout="vertical"
>
<Flex vertical gap={12}>
{source !== 'workflow' && <>
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t(`memoryConversation.web_search`)}
name={['web_search', "enabled"]}
/>
</div>
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t('application.text_to_speech')}
name={['text_to_speech', "enabled"]}
desc={t('application.text_to_speech_desc')}
/>
</div>
</>}
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t('application.file_upload')}
name={['file_upload', "enabled"]}
desc={values?.file_upload?.enabled ? undefined : t('application.file_upload_desc')}
/>
{values?.file_upload?.enabled && (() => {
const fu = values.file_upload
const types = [
{ type: 'image', enabled: fu.image_enabled, maxSize: fu.image_max_size_mb },
{ type: 'audio', enabled: fu.audio_enabled, maxSize: fu.audio_max_size_mb },
{ type: 'document', enabled: fu.document_enabled, maxSize: fu.document_max_size_mb },
{ type: 'video', enabled: fu.video_enabled, maxSize: fu.video_max_size_mb },
].filter(item => item.enabled)
return types.length > 0 ? <>
<Flex gap={12} className="rb:py-2!">
<div className="rb:flex-1 rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:bg-white rb:text-[12px]">
<div className="rb:grid rb:grid-cols-2 rb:gap-2 rb:text-[12px] rb:text-[#5B6167] rb:border-b rb:border-b-[#DFE4ED]">
<div className="rb:px-3 rb:py-1">{t(`application.supportedTypes`)}</div>
<div className="rb:px-3 rb:py-1">{t('application.singleMaxSize')}</div>
</div>
{types.map((item, index) => (
<div key={item.type} className={clsx('rb:grid rb:grid-cols-2 rb:gap-2', {
'rb:border-b rb:border-b-[#DFE4ED]': index !== types.length - 1
})}>
<div className="rb:px-3 rb:py-1">{t(`application.${item.type}`)}</div>
<div className="rb:px-3 rb:py-1">{item.maxSize} MB</div>
</div>
))}
</div>
<div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:py-1">{t('application.maxCount')}</div>
{fu.max_file_count} {t('application.unix')}
</div>
</Flex>
<Button block onClick={handleOpenSettings}>{t('application.setting')}</Button>
</> : <Button block onClick={handleOpenSettings}>{t('application.setting')}</Button>
})()}
<Form.Item name="file_upload" hidden />
</div>
</Flex>
</Form>
</RbModal>
<FileUploadSettingModal
ref={fileUploadSettingModalRef}
onSave={handleSaveSettings}
/>
</>
);
});
export default FeaturesConfigModal;

View File

@@ -0,0 +1,222 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-05
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-19 15:18:20
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, InputNumber, Flex, Switch, Row, Col, Radio } from 'antd';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import RbModal from '@/components/RbModal';
import type { FeaturesConfigForm } from '../../types'
type FileUpload = Omit<FeaturesConfigForm['file_upload'], 'settings'>
interface FileUploadSettingModalRef {
handleOpen: (values?: FileUpload) => void;
handleClose: () => void;
}
interface FileUploadSettingModalProps {
onSave: (values: FileUpload) => void;
}
const fileTypeOptions = [
{
type: 'document',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/txt.svg')]"></div>,
formats: [
"pdf",
"docx",
"doc",
"xlsx",
"xls",
"txt",
"csv",
"json",
"md",
],
},
{
type: 'image',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/image.svg')]"></div>,
formats: [
"png",
"jpg",
"jpeg"
],
},
{
type: 'audio',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/audio.svg')]"></div>,
formats: [
"mp3",
"wav",
"m4a",
],
},
{
type: 'video',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/video.svg')]"></div>,
formats: [
"mp4",
"mov",
],
},
];
const defaultValues: FileUpload = {
enabled: false,
image_enabled: false,
image_max_size_mb: 20,
image_allowed_extensions: [
"png",
"jpg",
"jpeg"
],
audio_enabled: false,
audio_max_size_mb: 50,
audio_allowed_extensions: [
"mp3",
"wav",
"m4a",
"ogg",
"flac"
],
document_enabled: false,
document_max_size_mb: 100,
document_allowed_extensions: [
"pdf",
"docx",
"xlsx",
"txt",
"csv",
"json"
],
video_enabled: false,
video_max_size_mb: 100,
video_allowed_extensions: [
"mp4",
"mov",
"avi",
"webm"
],
max_file_count: 5,
allowed_transfer_methods: 'both'
}
const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadSettingModalProps>(({
onSave,
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<FileUpload>();
const values = Form.useWatch([], form)
const handleClose = () => {
setVisible(false);
form.resetFields();
};
const handleOpen = (values?: FileUpload) => {
setVisible(true);
if (values) {
const methods = values.allowed_transfer_methods || ['local_file', 'remote_url']
const transferMethod = Array.isArray(methods)
? methods.length === 2 ? 'both' : methods[0]
: methods
form.setFieldsValue({ ...values, allowed_transfer_methods: transferMethod as any })
} else {
form.setFieldsValue(defaultValues)
}
};
const handleSave = async () => {
const vals = await form.validateFields();
const methodMap: Record<string, string[]> = {
local_file: ['local_file'],
remote_url: ['remote_url'],
both: ['local_file', 'remote_url'],
}
onSave({ ...vals, allowed_transfer_methods: methodMap[vals.allowed_transfer_methods as unknown as string] ?? [] });
handleClose();
};
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={t('application.settings')}
open={visible}
onCancel={handleClose}
onOk={handleSave}
>
<Form form={form} layout="vertical" initialValues={defaultValues}>
<Form.Item
label={t('application.uploadType')}
name="allowed_transfer_methods"
>
<Radio.Group block buttonStyle="solid">
<Radio.Button value="local_file">{t('application.local')}</Radio.Button>
<Radio.Button value="remote_url">URL</Radio.Button>
<Radio.Button value="both">{t('application.both')}</Radio.Button>
</Radio.Group>
</Form.Item>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-1">{t('application.maxCount')}</div>
<Form.Item label={t('application.maxCount')} name="max_file_count">
<InputNumber min={1} max={20} precision={0} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item label={t('application.supportedTypes')}>
<Flex vertical gap={12}>
{fileTypeOptions.map((option) => {
const enabledKey = `${option.type}_enabled` as keyof FileUpload
const sizeKey = `${option.type}_max_size_mb` as keyof FileUpload
const isEnabled = values?.[enabledKey]
return (
<div
key={option.type}
className={clsx('rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3', {
'rb:bg-[#f5f7fc]': isEnabled
})}
>
<Row gutter={12}>
<Col flex="36px" className="rb:self-center">{option.icon}</Col>
<Col flex="1">
<Flex align="center" justify="space-between">
<Flex vertical>
<div className="rb:font-medium">{t(`application.${option.type}`)}</div>
<div className="rb:text-[12px] rb:text-[#5B6167]">{option.formats.map(item => item.toUpperCase()).join(', ')}</div>
</Flex>
<Form.Item name={enabledKey} valuePropName="checked" noStyle>
<Switch />
</Form.Item>
</Flex>
</Col>
</Row>
{isEnabled && (
<Flex align="center" gap={12} className="rb:mt-3! rb:pt-3! rb:border-t rb:border-[#DFE4ED]">
<div>{t('application.singleMaxSize')}: </div>
<Form.Item name={sizeKey} noStyle>
<InputNumber min={1} max={100} suffix="MB" className="rb:flex-1" />
</Form.Item>
<Form.Item name={`${option.type}_allowed_extensions`} hidden />
</Flex>
)}
</div>
)
})}
</Flex>
</Form.Item>
</Form>
</RbModal>
);
});
export default FileUploadSettingModal;

View File

@@ -0,0 +1,54 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-13 17:20:21
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 15:38:59
*/
import { type FC, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from 'antd';
import FeaturesConfigModal from './FeaturesConfigModal'
import type { FeaturesConfigModalRef, FeaturesConfigForm } from '../../types'
import type { Application } from '@/views/ApplicationManagement/types';
/** Props for the FeaturesConfig component */
interface FeaturesConfigProps {
/** Current feature configuration values */
value: FeaturesConfigForm;
/** Callback to propagate updated config back to the parent */
refresh: (value: FeaturesConfigForm) => void;
source?: Application['type'];
}
const FeaturesConfig: FC<FeaturesConfigProps> = ({
value,
refresh,
source
}) => {
const { t } = useTranslation();
// Ref used to imperatively open the config modal
const funConfigModalRef = useRef<FeaturesConfigModalRef>(null)
/** Open the feature config modal pre-populated with the current values */
const handleFeaturesConfig = () => {
console.log('handleFeaturesConfig', value)
funConfigModalRef.current?.handleOpen(value)
}
return (
<>
{/* Button that triggers the feature configuration modal */}
<Button onClick={handleFeaturesConfig}>{t('application.features')}</Button>
{/* Modal for editing feature settings; calls refresh on save */}
<FeaturesConfigModal
ref={funConfigModalRef}
refresh={refresh}
source={source}
/>
</>
)
}
export default FeaturesConfig

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 10:15:39
* @Last Modified time: 2026-03-19 21:22:53
*/
/**
* Tool List Component
@@ -22,6 +22,7 @@ import type {
import Empty from '@/components/Empty'
import ToolModal from './ToolModal'
import { getToolMethods, getToolDetail } from '@/api/tools'
import Tag from '@/components/Tag'
/**
* Tool list management component
@@ -47,18 +48,19 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
const mcpFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
return {
...item,
is_active: (toolDetail as any).is_active,
label: mcpFilterItem?.description,
method_id: mcpFilterItem?.method_id,
value: mcpFilterItem?.name,
description: mcpFilterItem?.description,
parameters: mcpFilterItem?.parameters
}
break
case 'builtin':
if ((methods as any[]).length > 1) {
const builtinFilterItem = (methods as any[]).find(vo => vo.name === item.operation)
return {
...item,
is_active: (toolDetail as any).is_active,
label: builtinFilterItem?.description,
method_id: builtinFilterItem?.method_id,
value: builtinFilterItem?.name,
@@ -68,17 +70,18 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
}
return {
...item,
is_active: (toolDetail as any).is_active,
label: (methods as any[])[0]?.description,
method_id: (methods as any[])[0]?.method_id,
value: (methods as any[])[0]?.name,
description: (methods as any[])[0]?.description,
parameters: (methods as any[])[0]?.parameters
}
break
default:
const customFilterItem = (methods as any[]).find(vo => vo.method_id === item.operation)
return {
...item,
is_active: (toolDetail as any).is_active,
label: customFilterItem?.name,
method_id: customFilterItem?.method_id,
value: customFilterItem?.name,
@@ -103,7 +106,10 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
}
/** Add new tool to list */
const updateTools = (tool: ToolOption) => {
const list = [...toolList, tool]
const list = [...toolList, {
...tool,
is_active: true,
}]
setToolList(list)
onChange && onChange(list)
}
@@ -142,8 +148,13 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
: <Flex vertical gap={12}>
{toolList.map((item, index) => (
<Flex key={index} align="center" justify="space-between" className="rb:py-2.5! rb:pl-4! rb:pr-3! rb-border rb:rounded-lg">
<div className="rb:font-medium rb:leading-4">
{item.label}
<div>
<div className="rb:font-medium rb:leading-4">
{item.label}
</div>
<Tag color={item.is_active ? 'success' : 'error'} className="rb:mt-1">
{item.is_active ? t('common.enable') : t('common.deleted')}
</Tag>
</div>
<Space size={12}>
<Switch size="small" checked={item.enabled} onChange={() => handleChangeEnabled(index)} />

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:10
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:10
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 15:50:48
*/
/**
* Type definitions for tool configuration in application settings
@@ -32,6 +32,7 @@ export interface ToolOption {
tool_id?: string;
/** Whether tool is enabled */
enabled?: boolean;
is_active?: boolean;
}
/**

View File

@@ -2,21 +2,23 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:37
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 18:57:36
* @Last Modified time: 2026-03-19 21:09:32
*/
import React, { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router-dom';
import ConfigHeader from './components/ConfigHeader'
import type { AgentRef, ClusterRef, WorkflowRef } from './types'
import type { AgentRef, ClusterRef, WorkflowRef, Config } from './types'
import type { Application } from '@/views/ApplicationManagement/types'
import Agent from './Agent'
import Api from './Api'
import ReleasePage from './ReleasePage'
import Cluster from './Cluster'
import { getApplication } from '@/api/application'
import { getApplication, getApplicationConfig, getMultiAgentConfig, getWorkflowConfig } from '@/api/application'
import Workflow from '@/views/Workflow';
import Statistics from './Statistics'
import TestChat from './TestChat'
import type { WorkflowConfig } from '@/views/Workflow/types';
/**
* Application configuration page component
@@ -25,7 +27,7 @@ import Statistics from './Statistics'
*/
const ApplicationConfig: React.FC = () => {
// Hooks
const { id } = useParams();
const { id, source } = useParams();
// Refs for different application types
const agentRef = useRef<AgentRef>(null)
@@ -35,6 +37,32 @@ const ApplicationConfig: React.FC = () => {
// State
const [application, setApplication] = useState<Application | null>(null);
const [activeTab, setActiveTab] = useState('arrangement');
const [features, setFeatures] = useState<import('./types').FeaturesConfigForm | undefined>(undefined);
useEffect(() => {
setActiveTab(source === 'sharing' ? 'test' : 'arrangement')
}, [source])
const [config, setConfig] = useState<Config | WorkflowConfig | null>(null)
useEffect(() => {
if (source === 'sharing' && application?.type) {
getAppConfig()
}
}, [source, application?.type])
const getAppConfig = () => {
if (!id || !source || !application?.type) {
return
}
const request = application?.type === 'agent'
? getApplicationConfig
: application?.type === 'multi_agent'
? getMultiAgentConfig
: getWorkflowConfig
request(id as string).then(res => {
setConfig(res as Config | WorkflowConfig | null)
})
}
/**
* Handle tab change with auto-save for arrangement tab
@@ -87,14 +115,17 @@ const ApplicationConfig: React.FC = () => {
refresh={getApplicationInfo}
appRef={application?.type === 'agent' ? agentRef : application?.type === 'multi_agent' ? clusterRef : application?.type === 'workflow' ? workflowRef : undefined}
workflowRef={workflowRef}
features={features}
onFeaturesChange={setFeatures}
/>
<div className="rb:p-3 rb:max-h-[calc(100vh-65px)] rb:overflow-auto">
{activeTab === 'arrangement' && application?.type === 'agent' && <Agent ref={agentRef} />}
{activeTab === 'arrangement' && application?.type === 'multi_agent' && <Cluster ref={clusterRef} />}
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} />}
{activeTab === 'arrangement' && application?.type === 'agent' && <Agent ref={agentRef} onFeaturesLoad={setFeatures} />}
{activeTab === 'arrangement' && application?.type === 'multi_agent' && <Cluster ref={clusterRef} onFeaturesLoad={setFeatures} />}
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} onFeaturesLoad={setFeatures} />}
{activeTab === 'api' && <Api application={application} />}
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
{activeTab === 'statistics' && <Statistics application={application} />}
{activeTab === 'test' && <TestChat application={application} config={config} />}
</div>
</>
);

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 18:55:57
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-19 21:10:53
*/
import type { KnowledgeConfig } from './components/Knowledge/types'
import type { Variable } from './components/VariableList/types'
@@ -77,6 +77,8 @@ export interface Config extends MultiAgentConfig {
/** Last update timestamp */
updated_at: number;
skills?: SkillConfigForm | null;
features?: FeaturesConfigForm;
}
/**
@@ -127,6 +129,8 @@ export interface AgentRef {
* @param flag - Whether to show success message
*/
handleSave: (flag?: boolean) => Promise<unknown>;
features: Config['features'];
handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void;
}
/**
@@ -138,6 +142,8 @@ export interface ClusterRef {
* @param flag - Whether to show success message
*/
handleSave: (flag?: boolean) => Promise<unknown>;
features: Config['features'];
handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void;
}
/**
@@ -156,6 +162,8 @@ export interface WorkflowRef {
/** Add variable */
addVariable: () => void;
config: WorkflowConfig | null;
features: WorkflowConfig['features'];
handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void;
}
/**
@@ -402,4 +410,72 @@ export interface StatisticsData {
total_api_calls: number;
/** Total tokens used */
total_tokens: number;
}
export interface FileTypeConfig {
type: string;
enabled: boolean;
maxCount: number;
maxSize: number;
}
interface FileSetttings {
image_enabled: boolean;
image_max_size_mb: number;
image_allowed_extensions: string[];
audio_enabled: boolean;
audio_max_size_mb: number;
audio_allowed_extensions: string[];
document_enabled: boolean;
document_max_size_mb: number;
document_allowed_extensions: string[];
video_enabled: boolean;
video_max_size_mb: number;
video_allowed_extensions: string[];
max_file_count: number;
allowed_transfer_methods: string[] | string;
}
export type FeaturesConfigForm = {
file_upload: FileSetttings & {
enabled: boolean;
settings?: FileSetttings
};
opening_statement: {
enabled: boolean;
statement: string | null;
suggested_questions: string[];
};
suggested_questions_after_answer: {
enabled: boolean;
};
text_to_speech: {
enabled: boolean;
voice: string | null;
language: string | null;
autoplay: boolean;
};
citation: {
enabled: boolean;
};
web_search: {
enabled: boolean;
search_engine: string | null;
};
}
/**
* Function config modal ref methods
*/
export interface FeaturesConfigModalRef {
/** Open function config modal */
handleOpen: (value: FeaturesConfigForm) => void;
}
/**
* App sharing modal ref methods
*/
export interface AppSharingModalRef {
handleOpen: () => void;
}
export interface AppSharingForm {
target_workspace_ids: string[];
permission: 'readonly' | 'editable'
}