Merge branch 'develop' into feature/ui_upgrade_zy
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
485
web/src/views/ApplicationConfig/TestChat/index.tsx
Normal file
485
web/src/views/ApplicationConfig/TestChat/index.tsx
Normal 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
|
||||
8
web/src/views/ApplicationConfig/TestChat/type.ts
Normal file
8
web/src/views/ApplicationConfig/TestChat/type.ts
Normal 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
|
||||
}
|
||||
189
web/src/views/ApplicationConfig/components/AppSharingModal.tsx
Normal file
189
web/src/views/ApplicationConfig/components/AppSharingModal.tsx
Normal 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;
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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)} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
Reference in New Issue
Block a user