feat(web): app share
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-03 14:24:34
|
||||
* @Last Modified time: 2026-03-13 16:58:15
|
||||
*/
|
||||
import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import clsx from 'clsx'
|
||||
@@ -23,7 +23,8 @@ import type {
|
||||
MemoryConfig,
|
||||
AiPromptModalRef,
|
||||
Source,
|
||||
ChatVariableConfigModalRef
|
||||
ChatVariableConfigModalRef,
|
||||
FunConfigForm
|
||||
} from './types'
|
||||
import type { Variable } from './components/VariableList/types'
|
||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||
@@ -41,6 +42,7 @@ import ToolList from './components/ToolList/ToolList'
|
||||
import SkillList from './components/Skill'
|
||||
import ChatVariableConfigModal from './components/ChatVariableConfigModal';
|
||||
import type { Skill } from '@/views/Skills/types'
|
||||
import FunConfig from './components/FunConfig'
|
||||
|
||||
/**
|
||||
* Description wrapper component
|
||||
@@ -99,7 +101,7 @@ const SwitchWrapper: FC<{ title: string, desc?: string, name: string | string[];
|
||||
* @param name - Form field name
|
||||
* @param url - API URL for options
|
||||
*/
|
||||
const SelectWrapper: FC<{ title: string, desc: string, name: string | string[], url: string }> = ({ title, desc, name, url }) => {
|
||||
const SelectWrapper: FC<{ title: string, desc: string, name: string | string[], url: string; disabled?: boolean }> = ({ title, desc, name, url, disabled }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
@@ -115,6 +117,7 @@ const SelectWrapper: FC<{ title: string, desc: string, name: string | string[],
|
||||
hasAll={false}
|
||||
valueKey='config_id'
|
||||
labelKey="config_name"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
<DescWrapper desc={t(`application.${desc}`)} className="rb:mt-2" />
|
||||
@@ -352,7 +355,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
}, [modelList, values?.default_model_config_id])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleSave
|
||||
handleSave,
|
||||
funConfig: values?.funConfig
|
||||
}))
|
||||
|
||||
const aiPromptModalRef = useRef<AiPromptModalRef>(null)
|
||||
@@ -406,7 +410,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
useEffect(() => {
|
||||
setChatVariables(values?.variables || [])
|
||||
}, [values?.variables])
|
||||
console.log('values', values)
|
||||
|
||||
const handleSaveFunConfig = (value: FunConfigForm) => {
|
||||
form.setFieldValue('funConfig', value)
|
||||
}
|
||||
console.log('agent', values)
|
||||
return (
|
||||
<>
|
||||
{loading && <Spin fullscreen></Spin>}
|
||||
@@ -418,6 +426,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
{defaultModel?.name ? <div className="rb:w-4 rb:h-4 rb:bg-[url('@/assets/images/application/model.svg')] rb:group-hover:bg-[url('@/assets/images/application/model_hover.svg')]"></div> : null}
|
||||
{defaultModel?.name || t('application.chooseModel')}
|
||||
</Button>
|
||||
{/* <FunConfig value={values?.funConfig as FunConfigForm} refresh={handleSaveFunConfig} /> */}
|
||||
<Button type="primary" onClick={() => handleSave()}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
@@ -426,6 +435,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
<Form form={form}>
|
||||
<Form.Item name="default_model_config_id" hidden noStyle></Form.Item>
|
||||
<Form.Item name="model_parameters" hidden noStyle></Form.Item>
|
||||
<Form.Item name="funConfig" hidden noStyle></Form.Item>
|
||||
<Space size={16} direction="vertical" style={{ width: '100%' }}>
|
||||
<Card title={t('application.promptConfiguration')}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.75">
|
||||
@@ -464,11 +474,12 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
<Card title={t('application.memoryConfiguration')}>
|
||||
<Space size={24} direction='vertical' style={{ width: '100%' }}>
|
||||
<SwitchWrapper title="dialogueHistoricalMemory" desc="dialogueHistoricalMemoryDesc" name={['memory', 'enabled']} />
|
||||
<SelectWrapper
|
||||
title="selectMemoryContent"
|
||||
desc="selectMemoryContentDesc"
|
||||
<SelectWrapper
|
||||
title="selectMemoryContent"
|
||||
desc="selectMemoryContentDesc"
|
||||
name={['memory', 'memory_config_id']}
|
||||
url={memoryConfigListUrl}
|
||||
disabled={!values?.memory?.enabled}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
@@ -493,11 +504,6 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
{t('application.debuggingAndPreview')}
|
||||
|
||||
<Space size={10}>
|
||||
{chatVariables.length > 0 &&
|
||||
<Button type="primary" ghost onClick={handleOpenVariableConfig}>
|
||||
{t('application.variableConfig')}
|
||||
</Button>
|
||||
}
|
||||
<Button type="primary" ghost onClick={handleAddModel}>
|
||||
+ {t('application.addModel')}
|
||||
</Button>
|
||||
@@ -511,6 +517,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
updateChatList={setChatList}
|
||||
handleSave={handleSave}
|
||||
chatVariables={chatVariables}
|
||||
handleEditVariables={handleOpenVariableConfig}
|
||||
/>
|
||||
</RbCard>
|
||||
</Col>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:33
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:29:33
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-05 13:47:23
|
||||
*/
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -165,7 +165,8 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
setSubAgents(prev => prev.filter(item => item.agent_id !== agent.agent_id))
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleSave
|
||||
handleSave,
|
||||
funConfig: data?.funConfig
|
||||
}))
|
||||
|
||||
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:41
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:29:41
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-11 17:44:24
|
||||
*/
|
||||
import { type FC, useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -14,7 +14,8 @@ import RbCard from '@/components/RbCard/Card'
|
||||
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';
|
||||
@@ -39,6 +40,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[]>([])
|
||||
|
||||
@@ -129,6 +131,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
{data?.type !== 'multi_agent' && <Button onClick={handleExport}>{t('common.export')}</Button>}
|
||||
{data.current_release_id !== selectedVersion.id && <Button onClick={handleRollback}>{t('application.willRollToThisVersion')}</Button>}
|
||||
<Button type="primary" ghost onClick={() => releaseShareModalRef.current?.handleOpen()}>{t('application.share')}</Button>
|
||||
<Button type="primary" ghost onClick={() => appSharingModalRef.current?.handleOpen()}>{t('application.sharing')}</Button>
|
||||
</>}
|
||||
<Button type="primary" onClick={() => releaseModalRef.current?.handleOpen()}>{t('application.release')}</Button>
|
||||
</Space>
|
||||
@@ -178,6 +181,11 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
ref={releaseShareModalRef}
|
||||
version={selectedVersion}
|
||||
/>
|
||||
<AppSharingModal
|
||||
ref={appSharingModalRef}
|
||||
appId={data.id}
|
||||
version={selectedVersion}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
642
web/src/views/ApplicationConfig/TestChat/index.tsx
Normal file
642
web/src/views/ApplicationConfig/TestChat/index.tsx
Normal file
@@ -0,0 +1,642 @@
|
||||
import { type FC, useState, useRef, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { App, Flex, Dropdown, type MenuProps, Divider, Form, Space } from 'antd'
|
||||
import { SettingOutlined } from '@ant-design/icons'
|
||||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
|
||||
import VariableConfigModal from '@/views/Workflow/components/Chat/VariableConfigModal'
|
||||
import { draftRun } from '@/api/application';
|
||||
|
||||
import Empty from '@/components/Empty'
|
||||
import Chat from '@/components/Chat'
|
||||
import AudioRecorder from '@/components/AudioRecorder'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import UploadFiles from '@/views/Conversation/components/FileUpload'
|
||||
import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
|
||||
import Runtime from '@/views/Workflow/components/Chat/Runtime';
|
||||
import { nodeLibrary } from '@/views/Workflow/constant'
|
||||
// import ButtonCheckbox from '@/components/ButtonCheckbox';
|
||||
|
||||
// import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
|
||||
// import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
// import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
|
||||
// import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
|
||||
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import type { VariableConfigModalRef, WorkflowConfig } from '@/views/Workflow/types'
|
||||
import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types'
|
||||
import type { TestChatProps } from './type';
|
||||
import type { UploadFileListModalRef } from '@/views/Conversation/types'
|
||||
import type { SSEMessage } from '@/utils/stream'
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
files: any[];
|
||||
variables: Variable[]
|
||||
}
|
||||
const TestChat: FC<TestChatProps> = ({
|
||||
application,
|
||||
config
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { message: messageApi } = App.useApp()
|
||||
const variableConfigModalRef = useRef<VariableConfigModalRef>(null)
|
||||
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
|
||||
|
||||
const [loading, setLoading] = useState(false) // Send button loading state
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([]) // Chat message history
|
||||
const [streamLoading, setStreamLoading] = useState(false) // SSE streaming state
|
||||
const [conversationId, setConversationId] = useState<string | null>(null) // Current conversation ID
|
||||
const [message, setMessage] = useState<string | undefined>(undefined) // Current input message
|
||||
const [form] = Form.useForm<FormData>()
|
||||
const queryValues = Form.useWatch([], form)
|
||||
|
||||
useEffect(() => {
|
||||
getVariables()
|
||||
}, [application, config])
|
||||
|
||||
const getVariables = () => {
|
||||
if (!application || !config) return
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
form.setFieldValue('variables', [...initVariables])
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the variable configuration modal
|
||||
*/
|
||||
const handleEditVariables = () => {
|
||||
variableConfigModalRef.current?.handleOpen(queryValues.variables)
|
||||
}
|
||||
/**
|
||||
* Saves updated variable values from the modal
|
||||
*/
|
||||
const handleSave = (values: Variable[]) => {
|
||||
form.setFieldValue('variables', [...values])
|
||||
}
|
||||
/**
|
||||
* Handles file upload from local device
|
||||
*/
|
||||
const fileChange = (file?: any) => {
|
||||
form.setFieldValue('files', [...(queryValues.files || []), file])
|
||||
}
|
||||
const handleRecordingComplete = async (file: any) => {
|
||||
form.setFieldValue('files', [...(queryValues.files || []), file])
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles dropdown menu actions for file upload
|
||||
*/
|
||||
const handleShowUpload: MenuProps['onClick'] = ({ key }) => {
|
||||
switch(key) {
|
||||
case 'define':
|
||||
uploadFileListModalRef.current?.handleOpen()
|
||||
break
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds files from remote URL modal
|
||||
*/
|
||||
const addFileList = (list?: any[]) => {
|
||||
if (!list || list.length <= 0) return
|
||||
form.setFieldValue('files', [...(queryValues.files || []), ...(list || [])])
|
||||
}
|
||||
/**
|
||||
* Updates the entire file list (used when removing files)
|
||||
*/
|
||||
const updateFileList = (list?: any[]) => {
|
||||
form.setFieldValue('files', [...list || []])
|
||||
}
|
||||
const isNeedVariableConfig = useMemo(() => {
|
||||
return queryValues?.variables.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === ''))
|
||||
}, [queryValues?.variables])
|
||||
|
||||
const addUserMessage = (message: string, files: any[]) => {
|
||||
const newUserMessage: ChatItem = {
|
||||
role: 'user',
|
||||
content: message,
|
||||
created_at: Date.now(),
|
||||
files
|
||||
};
|
||||
setChatList(prev => [...prev, newUserMessage])
|
||||
}
|
||||
const addAssistantMessage = () => {
|
||||
const { type } = application || {}
|
||||
setChatList(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
subContent: type === 'workflow' ? [] : undefined,
|
||||
}])
|
||||
}
|
||||
|
||||
const updateAssistantMessage = (content: string) => {
|
||||
setChatList(prev => {
|
||||
let newList = [...prev]
|
||||
const lastMsg = newList[newList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
lastMsg.content += content
|
||||
}
|
||||
return newList
|
||||
})
|
||||
}
|
||||
const updateErrorAssistantMessage = (message_length: number) => {
|
||||
if (message_length > 0) return
|
||||
setChatList(prev => {
|
||||
let newList = [...prev]
|
||||
const lastMsg = newList[newList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
lastMsg.content = null
|
||||
}
|
||||
return newList
|
||||
})
|
||||
}
|
||||
const handleSend = () => {
|
||||
if (loading || !application || !message || !message?.trim()) return
|
||||
// Validate required variables before sending
|
||||
const { variables, files } = queryValues;
|
||||
let isCanSend = true
|
||||
const params: Record<string, any> = {}
|
||||
if (variables && variables.length > 0) {
|
||||
const needRequired: string[] = []
|
||||
variables.forEach(vo => {
|
||||
params[vo.name] = vo.value
|
||||
|
||||
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
|
||||
isCanSend = false
|
||||
needRequired.push(vo.name)
|
||||
}
|
||||
})
|
||||
|
||||
if (needRequired.length) {
|
||||
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
|
||||
}
|
||||
}
|
||||
if (!isCanSend) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
addUserMessage(message, files)
|
||||
setMessage(undefined)
|
||||
form.setFieldValue('files', [])
|
||||
addAssistantMessage()
|
||||
setStreamLoading(true)
|
||||
setLoading(true)
|
||||
|
||||
draftRun(
|
||||
application.id,
|
||||
formatParams(message, conversationId, files, params),
|
||||
handleStreamMessage
|
||||
)
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setStreamLoading(false)
|
||||
})
|
||||
}
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
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) {
|
||||
setConversationId(conversation_id);
|
||||
}
|
||||
break
|
||||
case 'message':
|
||||
updateAssistantMessage(content)
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
setConversationId(conversation_id);
|
||||
}
|
||||
break;
|
||||
case 'end':
|
||||
updateErrorAssistantMessage(message_length)
|
||||
setStreamLoading(false)
|
||||
break;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const handleWorkflowSend = () => {
|
||||
if (loading || !application || !message || !message?.trim()) return
|
||||
|
||||
// Validate required variables before sending
|
||||
const { variables, files } = queryValues;
|
||||
let isCanSend = true
|
||||
const params: Record<string, any> = {}
|
||||
if (variables.length > 0) {
|
||||
const needRequired: string[] = []
|
||||
variables.forEach(vo => {
|
||||
params[vo.name] = vo.value ?? vo.defaultValue
|
||||
|
||||
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
|
||||
isCanSend = false
|
||||
needRequired.push(vo.name)
|
||||
}
|
||||
})
|
||||
|
||||
if (needRequired.length) {
|
||||
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
|
||||
}
|
||||
}
|
||||
if (!isCanSend) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
addUserMessage(message, files)
|
||||
addAssistantMessage()
|
||||
form.setFieldsValue({
|
||||
files: [],
|
||||
})
|
||||
|
||||
setMessage(undefined)
|
||||
setStreamLoading(true)
|
||||
draftRun(
|
||||
application.id,
|
||||
formatParams(message, conversationId, files, params),
|
||||
handleWorkflowStreamMessage
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log('draftRun error', error)
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
if (lastIndex >= 0) {
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
status: 'failed',
|
||||
content: null,
|
||||
subContent: error.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 } = data as {
|
||||
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'
|
||||
};
|
||||
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,
|
||||
}
|
||||
}
|
||||
return newList
|
||||
})
|
||||
}
|
||||
|
||||
console.log('queryValues', queryValues)
|
||||
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)]': !queryValues?.files?.length,
|
||||
'rb:h-[calc(100%-208px)]': !!queryValues?.files?.length,
|
||||
})}
|
||||
data={chatList}
|
||||
streamLoading={streamLoading}
|
||||
loading={loading}
|
||||
onChange={setMessage}
|
||||
onSend={application?.type === 'workflow' ? handleWorkflowSend : handleSend}
|
||||
fileList={queryValues?.files || []}
|
||||
fileChange={updateFileList}
|
||||
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) => {
|
||||
return <Runtime item={item} index={index} />
|
||||
} : undefined}
|
||||
>
|
||||
<Form form={form}>
|
||||
<Flex justify="space-between" className="rb:flex-1">
|
||||
<Space size={8} align="center">
|
||||
<Form.Item name="files" noStyle>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'define', label: t('memoryConversation.addRemoteFile') },
|
||||
{
|
||||
key: 'upload', label: (
|
||||
<UploadFiles
|
||||
onChange={fileChange}
|
||||
/>
|
||||
)
|
||||
},
|
||||
],
|
||||
onClick: handleShowUpload
|
||||
}}
|
||||
>
|
||||
<Flex align="center" justify="center" className="rb:size-7 rb:cursor-pointer rb:rounded-[14px] rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6]">
|
||||
<div
|
||||
className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')]"
|
||||
></div>
|
||||
</Flex>
|
||||
</Dropdown>
|
||||
</Form.Item>
|
||||
{/* <Form.Item name="web_search" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={OnlineIcon}
|
||||
checkedIcon={OnlineCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.web_search`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
<Tooltip title={t(`memoryConversation.memory`)}></Tooltip>
|
||||
<Form.Item name="memory" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={MemoryFunctionIcon}
|
||||
checkedIcon={MemoryFunctionCheckedIcon}
|
||||
cicle={true}
|
||||
>
|
||||
</ButtonCheckbox>
|
||||
</Form.Item> */}
|
||||
<Form.Item name="variables" className="rb:mb-0!" hidden={!queryValues?.variables?.length}>
|
||||
<div
|
||||
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]", {
|
||||
'rb:border-[#FF5D34] rb:text-[#FF5D34]': isNeedVariableConfig,
|
||||
'rb:border-[#DFE4ED]': !isNeedVariableConfig,
|
||||
})}
|
||||
onClick={handleEditVariables}
|
||||
>
|
||||
<SettingOutlined className="rb:mr-1" />
|
||||
{t(`memoryConversation.variableConfig`)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space size={8} align="center">
|
||||
<AudioRecorder
|
||||
onRecordingComplete={handleRecordingComplete}
|
||||
/>
|
||||
<Divider type="vertical" className="rb:ml-0! rb:mr-2!" />
|
||||
</Space>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Chat>
|
||||
|
||||
<VariableConfigModal
|
||||
ref={variableConfigModalRef}
|
||||
refresh={handleSave}
|
||||
/>
|
||||
|
||||
<UploadFileListModal
|
||||
ref={uploadFileListModalRef}
|
||||
refresh={addFileList}
|
||||
/>
|
||||
</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
|
||||
}
|
||||
183
web/src/views/ApplicationConfig/components/AppSharingModal.tsx
Normal file
183
web/src/views/ApplicationConfig/components/AppSharingModal.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-13 17:19:13
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 17:26:57
|
||||
*/
|
||||
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 = form.getFieldValue('target_workspace_ids') as string[] ?? [];
|
||||
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
|
||||
name="target_workspace_ids"
|
||||
label={t('application.selectTargetSpace')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<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} // already-shared workspaces cannot be unselected
|
||||
onChange={() => handleToggle(space.id, isShared)}
|
||||
/>
|
||||
<span className="rb:flex-1 rb:text-sm">{space.name}</span>
|
||||
{/* Badge shown when the app is already shared with this workspace */}
|
||||
{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-13 15:20:32
|
||||
*/
|
||||
/**
|
||||
* Chat debugging component for application testing
|
||||
@@ -13,7 +13,8 @@
|
||||
import { type FC, useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd'
|
||||
import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd';
|
||||
import { SettingOutlined } from '@ant-design/icons'
|
||||
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
|
||||
@@ -45,13 +46,17 @@ interface ChatProps {
|
||||
/** Source type: multi-agent cluster or single agent */
|
||||
source?: 'multi_agent' | 'agent';
|
||||
chatVariables?: Variable[]; // Add chatVariables prop
|
||||
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 { message: messageApi } = App.useApp()
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -434,6 +439,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
const updateFileList = (list?: any[]) => {
|
||||
setFileList([...list || []])
|
||||
}
|
||||
const isNeedVariableConfig = chatVariables?.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === ''))
|
||||
|
||||
return (
|
||||
<div className="rb:relative rb:h-full rb:flex rb:flex-col">
|
||||
@@ -521,6 +527,18 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
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>
|
||||
{chatVariables && chatVariables.length > 0 && (
|
||||
<div
|
||||
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8] rb:text-[#212332]", {
|
||||
'rb:border-[#FF5D34] rb:text-[#FF5D34]': isNeedVariableConfig,
|
||||
'rb:border-[#DFE4ED]': !isNeedVariableConfig,
|
||||
})}
|
||||
onClick={handleEditVariables}
|
||||
>
|
||||
<SettingOutlined className="rb:mr-1" />
|
||||
{t(`memoryConversation.variableConfig`)}
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex align="center">
|
||||
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:27:52
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 16:48:52
|
||||
* @Last Modified time: 2026-03-13 17:12:59
|
||||
*/
|
||||
import { type FC, useRef, useMemo } from 'react';
|
||||
import { type FC, useRef, useMemo, useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Layout, Tabs, Dropdown, Button, Flex } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
@@ -18,9 +18,10 @@ 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 type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef, FunConfigForm } from '../types'
|
||||
import { deleteApplication, appExport } from '@/api/application'
|
||||
import CopyModal from './CopyModal'
|
||||
import FunConfig from './FunConfig'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
@@ -28,6 +29,11 @@ const { Header } = Layout;
|
||||
* Tab keys for application configuration
|
||||
*/
|
||||
const tabKeys = ['arrangement', 'api', 'release', 'statistics']
|
||||
const sharingTabKeys = [
|
||||
'test',
|
||||
// 'log',
|
||||
'api'
|
||||
]
|
||||
|
||||
/**
|
||||
* Menu icon mapping
|
||||
@@ -64,22 +70,23 @@ interface ConfigHeaderProps {
|
||||
const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
application, activeTab, handleChangeTab, refresh,
|
||||
workflowRef,
|
||||
appRef,
|
||||
}) => {
|
||||
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
|
||||
*/
|
||||
@@ -160,6 +167,13 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
return items
|
||||
}, [t, handleClick, application])
|
||||
|
||||
const funConfig = useMemo(() => {
|
||||
return (appRef?.current?.funConfig || { file_type: [] }) as FunConfigForm
|
||||
}, [appRef])
|
||||
const handleSaveFunConfig = useCallback((value: FunConfigForm) => {
|
||||
appRef?.current?.handleSaveFunConfig?.(value)
|
||||
}, [appRef])
|
||||
|
||||
console.log('formatMenuItems', formatMenuItems)
|
||||
return (
|
||||
<>
|
||||
@@ -170,7 +184,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="rb:max-w-[100%-80px] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{application?.name}</div>
|
||||
<Dropdown
|
||||
{source !== 'sharing' && <Dropdown
|
||||
menu={{ items: formatMenuItems, onClick: handleClick }}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
@@ -178,19 +192,20 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
></div>
|
||||
</Dropdown>
|
||||
</Dropdown>}
|
||||
</div>
|
||||
|
||||
<div className="rb:flex rb:justify-center">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={formatTabItems()}
|
||||
items={formatTabItems}
|
||||
onChange={handleChangeTab}
|
||||
className={styles.tabs}
|
||||
/>
|
||||
</div>
|
||||
{application?.type === 'workflow'
|
||||
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
|
||||
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
|
||||
{/* <FunConfig value={funConfig} refresh={handleSaveFunConfig} /> */}
|
||||
<Button onClick={clear}>{t('workflow.clear')}</Button>
|
||||
<Button onClick={addvariable}>{t('workflow.addvariable')}</Button>
|
||||
<Button onClick={run}>{t('workflow.run')}</Button>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-05
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-11 15:42:13
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Radio, InputNumber, Flex, Switch, Row, Col } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import RbModal from '@/components/RbModal';
|
||||
import type { FunConfigForm } from '../../types'
|
||||
|
||||
interface FileUploadSettingModalRef {
|
||||
handleOpen: (values?: FileUploadSettings) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
interface FileUploadSettings extends Omit<FunConfigForm, 'enabled'> {}
|
||||
|
||||
interface FileUploadSettingModalProps {
|
||||
onSave: (values: FileUploadSettings) => void;
|
||||
}
|
||||
|
||||
const fileTypeOptions = [
|
||||
{
|
||||
type: 'document',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/txt.svg')]"></div>,
|
||||
formats: 'TXT, MD, MDX, MARKDOWN, PDF, DOC, DOCX',
|
||||
defaultMaxCount: 1,
|
||||
defaultMaxSize: 2
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/image.svg')]"></div>,
|
||||
formats: 'JPG, JPEG, PNG, GIF, WEBP, SVG',
|
||||
defaultMaxCount: 1,
|
||||
defaultMaxSize: 2
|
||||
},
|
||||
{
|
||||
type: 'audio',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/audio.svg')]"></div>,
|
||||
formats: 'MP3, M4A, WAV, AMR, MPGA',
|
||||
defaultMaxCount: 1,
|
||||
defaultMaxSize: 2
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/video.svg')]"></div>,
|
||||
formats: 'MP4, MOV, MPEG, WEBM',
|
||||
defaultMaxCount: 1,
|
||||
defaultMaxSize: 2
|
||||
},
|
||||
];
|
||||
|
||||
const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadSettingModalProps>(({
|
||||
onSave,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const values = Form.useWatch([], form)
|
||||
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleOpen = (values?: FileUploadSettings) => {
|
||||
setVisible(true);
|
||||
// if (values) {
|
||||
// form.setFieldsValue(values);
|
||||
// }
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
onSave(values);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.settings')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSave}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
uploadType: 'both',
|
||||
fileTypes: fileTypeOptions.map(opt => ({
|
||||
type: opt.type,
|
||||
enabled: false,
|
||||
maxCount: opt.defaultMaxCount,
|
||||
maxSize: opt.defaultMaxSize
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('application.uploadType')}
|
||||
name="uploadType"
|
||||
>
|
||||
<Radio.Group block buttonStyle="solid">
|
||||
<Radio.Button value="local">{t('application.local')}</Radio.Button>
|
||||
<Radio.Button value="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
|
||||
name="maxCount"
|
||||
label={t('application.maxCount')}
|
||||
>
|
||||
<InputNumber min={1} max={100} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('application.supportedTypes')}>
|
||||
<Form.List name="fileTypes">
|
||||
{(fields) => (
|
||||
<Flex vertical gap={12}>
|
||||
{fields.map((field, index) => {
|
||||
const option = fileTypeOptions[index];
|
||||
const isEnabled = values?.fileTypes?.[index]?.enabled;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={field.key}
|
||||
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}</div>
|
||||
</Flex>
|
||||
<Form.Item name={[field.name, 'enabled']} 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={[field.name, 'maxSize']} noStyle>
|
||||
<InputNumber min={1} max={500} suffix="MB" className="rb:flex-1" />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
)}
|
||||
<Form.Item name={[field.name, 'type']} hidden>
|
||||
<input type="hidden" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default FileUploadSettingModal;
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:27:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 17:20:30
|
||||
*/
|
||||
/**
|
||||
* 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 type { FunConfigModalRef } from '../../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import type { FunConfigForm } from '../../types'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
import FileUploadSettingModal from './FileUploadSettingModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface FunConfigModalProps {
|
||||
refresh: (value: FunConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal for copying applications
|
||||
*/
|
||||
const FunConfigModal = forwardRef<FunConfigModalRef, FunConfigModalProps>(({
|
||||
refresh,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<FunConfigForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const values = Form.useWatch([], form)
|
||||
const fileUploadSettingModalRef = useRef<any>(null)
|
||||
|
||||
/** Close modal and reset form */
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
/** Open modal */
|
||||
const handleOpen = (initValue: FunConfigForm) => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue(initValue)
|
||||
};
|
||||
/** Copy application with new name */
|
||||
const handleSave = () => {
|
||||
setVisible(false);
|
||||
setLoading(true)
|
||||
const values = form.getFieldsValue()
|
||||
refresh(values)
|
||||
}
|
||||
|
||||
const handleOpenSettings = () => {
|
||||
fileUploadSettingModalRef.current?.handleOpen(values)
|
||||
}
|
||||
|
||||
const handleSaveSettings = (settings: any) => {
|
||||
form.setFieldsValue(settings)
|
||||
}
|
||||
|
||||
/** Expose methods to parent component */
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<RbModal
|
||||
title={t('application.funConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.copy')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Flex vertical gap={12}>
|
||||
<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.textTranfer')}
|
||||
name={['textTranfer', "enabled"]}
|
||||
desc={t('application.textTranferDesc')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
|
||||
<SwitchFormItem
|
||||
title={t('application.fileUpload')}
|
||||
name={['fileUpload', "enabled"]}
|
||||
desc={values?.fileUpload?.enabled ? undefined : t('application.fileUploadDesc')}
|
||||
/>
|
||||
{values?.fileUpload?.enabled && values?.fileTypes?.length > 0 ? <>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-2 rb:text-[12px] rb:text-[#5B6167]">
|
||||
<div>{t(`application.supportedTypes`)}</div>
|
||||
<div>{t('application.maxCount')}</div>
|
||||
<div>{t('application.singleMaxSize')}</div>
|
||||
</div>
|
||||
{values?.fileTypes?.filter(item => item.enabled).map(item => (
|
||||
<div key={item.type} className="rb:grid rb:grid-cols-3 rb:gap-2">
|
||||
<div>{t(`application.${item.type}`)}</div>
|
||||
<div>{item.maxCount} {t('application.unix')}</div>
|
||||
<div>{item.maxSize} MB</div>
|
||||
</div>
|
||||
))}
|
||||
<Button block onClick={handleOpenSettings}>{t('application.setting')}</Button>
|
||||
</> : null}
|
||||
<FormItem name="fileTypes" noStyle hidden></FormItem>
|
||||
<FormItem name="uploadType" noStyle hidden></FormItem>
|
||||
</div>
|
||||
</Flex>
|
||||
</Form>
|
||||
</RbModal>
|
||||
|
||||
<FileUploadSettingModal
|
||||
ref={fileUploadSettingModalRef}
|
||||
onSave={handleSaveSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default FunConfigModal;
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-13 17:20:21
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-13 17:20:21
|
||||
*/
|
||||
import { type FC, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'antd';
|
||||
|
||||
import FunConfigModal from './FunConfigModal'
|
||||
import type { FunConfigModalRef, FunConfigForm } from '../../types'
|
||||
|
||||
/** Props for the FunConfig component */
|
||||
interface FunConfigProps {
|
||||
/** Current feature configuration values */
|
||||
value: FunConfigForm;
|
||||
/** Callback to propagate updated config back to the parent */
|
||||
refresh: (value: FunConfigForm) => void;
|
||||
}
|
||||
|
||||
const FunConfig: FC<FunConfigProps> = ({
|
||||
value,
|
||||
refresh
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
// Ref used to imperatively open the config modal
|
||||
const funConfigModalRef = useRef<FunConfigModalRef>(null)
|
||||
|
||||
/** Open the feature config modal pre-populated with the current values */
|
||||
const handleFunConfig = () => {
|
||||
console.log('funConfig', value)
|
||||
funConfigModalRef.current?.handleOpen(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Button that triggers the feature configuration modal */}
|
||||
<Button onClick={handleFunConfig}>{t('application.funConfig')}</Button>
|
||||
|
||||
{/* Modal for editing feature settings; calls refresh on save */}
|
||||
<FunConfigModal
|
||||
ref={funConfigModalRef}
|
||||
refresh={refresh}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FunConfig
|
||||
@@ -1,22 +1,24 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:37
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 16:29:37
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-12 10:23:18
|
||||
*/
|
||||
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)
|
||||
@@ -36,6 +38,31 @@ const ApplicationConfig: React.FC = () => {
|
||||
const [application, setApplication] = useState<Application | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('arrangement');
|
||||
|
||||
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
|
||||
* @param key - New tab key
|
||||
@@ -94,6 +121,7 @@ const ApplicationConfig: React.FC = () => {
|
||||
{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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 16:40:30
|
||||
* @Last Modified time: 2026-03-13 17:01:04
|
||||
*/
|
||||
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;
|
||||
|
||||
funConfig?: FunConfigForm;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,6 +129,8 @@ export interface AgentRef {
|
||||
* @param flag - Whether to show success message
|
||||
*/
|
||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||
funConfig: Config['funConfig'];
|
||||
handleSaveFunConfig?: (value: FunConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,6 +142,8 @@ export interface ClusterRef {
|
||||
* @param flag - Whether to show success message
|
||||
*/
|
||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||
funConfig: Config['funConfig'];
|
||||
handleSaveFunConfig?: (value: FunConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,6 +162,8 @@ export interface WorkflowRef {
|
||||
/** Add variable */
|
||||
addVariable: () => void;
|
||||
config: WorkflowConfig | null;
|
||||
funConfig: WorkflowConfig['funConfig'];
|
||||
handleSaveFunConfig?: (value: FunConfigForm) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,4 +408,34 @@ export interface StatisticsData {
|
||||
total_api_calls: number;
|
||||
/** Total tokens used */
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
export interface FileTypeConfig {
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
maxCount: number;
|
||||
maxSize: number;
|
||||
}
|
||||
export interface FunConfigForm {
|
||||
enabled: boolean;
|
||||
fileTypes: FileTypeConfig[]
|
||||
uploadType: 'local' | 'url' | 'both';
|
||||
}
|
||||
/**
|
||||
* Function config modal ref methods
|
||||
*/
|
||||
export interface FunConfigModalRef {
|
||||
/** Open function config modal */
|
||||
handleOpen: (value: FunConfigForm) => 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