Merge branch 'develop' into release/v0.2.7

This commit is contained in:
Ke Sun
2026-03-16 15:47:00 +08:00
committed by GitHub
155 changed files with 13164 additions and 1796 deletions

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:21
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-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>

View File

@@ -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)

View File

@@ -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>
);
}

View 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

View File

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

View File

@@ -0,0 +1,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;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:39
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-05 17:03:46
* @Last Modified time: 2026-03-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} />

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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} />}
</>
);
};

View File

@@ -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'
}

View File

@@ -0,0 +1,158 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 17:36:16
*/
import React, { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, App, Flex, Row, Col, Collapse } from 'antd';
import clsx from 'clsx';
import type { MySharedOutItem } from './types';
import { mySharedOutList, cancelShare, cancelSpaceShare } from '@/api/application'
const MySharing: React.FC = () => {
const { t } = useTranslation();
const { modal } = App.useApp();
const [data, setData] = useState<MySharedOutItem[]>([])
useEffect(() => { getList() }, [])
const getList = () => {
mySharedOutList().then(res => setData(res as MySharedOutItem[]))
}
/** Group items by target_workspace_id */
const grouped = useMemo(() => {
const map = new Map<string, { workspace: Pick<MySharedOutItem, 'target_workspace_id' | 'target_workspace_name' | 'target_workspace_icon'>, items: MySharedOutItem[] }>();
data.forEach(item => {
if (!map.has(item.target_workspace_id)) {
map.set(item.target_workspace_id, {
workspace: {
target_workspace_id: item.target_workspace_id,
target_workspace_name: item.target_workspace_name,
target_workspace_icon: item.target_workspace_icon,
},
items: [],
});
}
map.get(item.target_workspace_id)!.items.push(item);
});
return Array.from(map.values());
}, [data]);
const handleAllCancel = (workspace: { target_workspace_name: string; target_workspace_id: string; }) => {
modal.confirm({
title: t('application.confirmWorkspaceCancelShareDesc', { workspace: workspace.target_workspace_name }),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
cancelSpaceShare(workspace.target_workspace_id)
.then(() => {
getList();
})
}
});
};
const handleCancelOne = (item: MySharedOutItem) => {
modal.confirm({
title: t('application.confirmAppCancelShareDesc', { app: item.source_app_name, workspace: item.target_workspace_name }),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
cancelShare(item.source_app_id, item.target_workspace_id)
.then(() => {
getList();
})
}
});
};
return (
<Flex vertical gap={12}>
{grouped.map(({ workspace, items }) => (
<Collapse
key={workspace.target_workspace_id}
defaultActiveKey={[workspace.target_workspace_id]}
items={[{
key: workspace.target_workspace_id,
label: (
<Flex align="center" gap={12}>
{workspace.target_workspace_icon
? <img src={workspace.target_workspace_icon} className="rb:w-8 rb:h-8 rb:rounded-lg rb:object-cover" />
: <div className="rb:w-8 rb:h-8 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
{workspace.target_workspace_name[0]}
</div>
}
<div>
<span className="rb:font-medium">{workspace.target_workspace_name}</span>
<div className="rb:text-[#5B6167] rb:text-[12px]">{t('application.appCount', { count: items.length })}</div>
</div>
</Flex>
),
extra: (
<Button
size="small"
onClick={e => { e.stopPropagation(); handleAllCancel(workspace); }}
>
{t('application.allCancel')}
</Button>
),
children: (
<Row gutter={[12, 12]}>
{items.map(item => (
<Col key={item.id} span={6} className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-3! rb:px-4! rb:relative">
<div
className="rb:absolute rb:top-3 rb:right-3 rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/close.svg')]"
onClick={() => handleCancelOne(item)}
/>
<Flex gap={8} align="center">
<div className="rb:size-7 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
{item.source_app_name[0]}
</div>
<div className="rb:font-medium">{item.source_app_name}</div>
</Flex>
<Flex vertical gap={4} className="rb:mt-3! rb:text-[12px]!">
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.type')}</span>
<span className={clsx({
'rb:text-[#155EEF] rb:font-medium': item.source_app_type === 'agent',
'rb:text-[#369F21] rb:font-medium': item.source_app_type === 'multi_agent',
})}>
{t(`application.${item.source_app_type}`)}
</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.version')}</span>
<span>{item.source_app_version}</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.permission')}</span>
<span className={clsx({
'rb:text-[#369F21] rb:font-medium': item.permission === 'editable',
'rb:text-[#5B6167] rb:font-medium': item.permission === 'readonly',
})}>
{t(`application.${item.permission}`)}
</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.souceStatus')}</span>
<span>{item.source_app_is_active ? t('application.sourceActive') : t('application.sourceInactive')}</span>
</Flex>
</Flex>
</Col>
))}
</Row>
),
}]}
/>
))}
</Flex>
);
};
export default MySharing;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-02 17:48:51
* @Last Modified time: 2026-03-16 09:56:02
*/
/**
* Application Management Page
@@ -10,9 +10,9 @@
* Supports creating, editing, and deleting applications
*/
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Row, Col, App, Select, Space, Dropdown } from 'antd';
import { Button, App, Select, Space, Dropdown, type SegmentedProps, Flex, Form } from 'antd';
import clsx from 'clsx';
import { DeleteOutlined } from '@ant-design/icons';
import { useSearchParams } from 'react-router-dom'
@@ -21,12 +21,16 @@ import ApplicationModal, { types } from './components/ApplicationModal';
import type { Application, ApplicationModalRef, Query, UploadWorkflowModalRef } from './types';
import SearchInput from '@/components/SearchInput'
import RbCard from '@/components/RbCard/Card'
import { getApplicationListUrl, deleteApplication } from '@/api/application'
import { getApplicationListUrl, deleteApplication, copyApplication } from '@/api/application'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { formatDateTime } from '@/utils/format';
import UploadWorkflowModal from './components/UploadWorkflowModal'
import UploadModal from './components/UploadModal'
import PageTabs from '@/components/PageTabs'
import MySharing from './MySharing'
const tabKeys = ['apps', 'sharing', 'myShare']
/**
* Application management main component
*/
@@ -34,20 +38,19 @@ const ApplicationManagement: React.FC = () => {
const { t } = useTranslation();
const { modal } = App.useApp();
const [searchParams] = useSearchParams()
const [query, setQuery] = useState<Query>({} as Query);
const applicationModalRef = useRef<ApplicationModalRef>(null);
const scrollListRef = useRef<PageScrollListRef>(null)
const uploadWorkflowModalRef = useRef<UploadWorkflowModalRef>(null);
const uploadModalRef = useRef<UploadWorkflowModalRef>(null);
const [form] = Form.useForm<Query>()
const query = Form.useWatch([], form)
const [activeTab, setActiveTab] = useState('apps');
useEffect(() => {
// Convert URLSearchParams to a plain object for easier access
const data = Object.fromEntries(searchParams)
const { type } = data
setQuery(prev => ({
...prev,
type: type || undefined
}))
form.setFieldValue('type', type || undefined)
}, [searchParams])
/** Refresh application list */
@@ -61,7 +64,11 @@ const ApplicationManagement: React.FC = () => {
}
/** Navigate to application configuration page */
const handleEdit = (item: Application) => {
window.open(`/#/application/config/${item.id}`);
let url = `/#/application/config/${item.id}`
if (item.is_shared) {
url += `/${activeTab}`
}
window.open(url);
}
/** Delete application with confirmation */
const handleDelete = (item: Application) => {
@@ -81,9 +88,6 @@ const ApplicationManagement: React.FC = () => {
}
})
}
const handleChangeType = (value?: string) => {
setQuery(prev => ({...prev, type: value}))
}
const handleImport = () => {
uploadWorkflowModalRef.current?.handleOpen()
@@ -97,90 +101,137 @@ const ApplicationManagement: React.FC = () => {
uploadModalRef.current?.handleOpen()
}
}
const formatTabItems = useMemo(() => {
return tabKeys.map(value => ({
value,
label: t(`application.${value}`),
}))
}, [tabKeys, t])
/** Handle tab change */
const handleChangeTab = (value: SegmentedProps['value']) => {
setActiveTab(value as string);
form.resetFields()
}
const handleCopy = (item: Application) => {
modal.confirm({
title: t('application.confirmCopyDesc', { app: item.name }),
okText: t('common.copy'),
cancelText: t('common.cancel'),
onOk: () => {
copyApplication(item.id)
.then(() => {
setActiveTab('apps')
})
}
});
}
return (
<>
<Row gutter={16} className="rb:mb-4">
<Col span={4}>
<Select
value={query.type}
placeholder={t('application.applicationType')}
options={types.map((type) => ({
value: type,
label: t(`application.${type}`),
}))}
allowClear
className="rb:w-full"
onChange={handleChangeType}
/>
</Col>
<Col span={8}>
<SearchInput
placeholder={t('application.searchPlaceholder')}
onSearch={(value) => setQuery({ search: value })}
style={{width: '100%'}}
/>
</Col>
<Col span={12} className="rb:text-right">
<Space size={12}>
<Dropdown
menu={{ items: [
{ key: 'thirdParty', label: t('application.importWorkflow') },
{ key: 'import', label: t('application.import') },
], onClick: handleClick }}
placement="bottomRight"
<Flex justify="space-between" className="rb:mb-3!">
<PageTabs
value={activeTab}
options={formatTabItems}
onChange={handleChangeTab}
/>
<Form
form={form}
initialValues={{}}
>
{activeTab !== 'myShare' &&
<Space size={8}>
<Form.Item name="type" noStyle>
<Select
placeholder={t('application.applicationType')}
options={types.map((type) => ({
value: type,
label: t(`application.${type}`),
}))}
allowClear
className="rb:w-30"
/>
</Form.Item>
<Form.Item name="search" noStyle>
<SearchInput
placeholder={t('application.searchPlaceholder')}
className="rb:w-75!"
/>
</Form.Item>
{activeTab === 'apps' && <>
<Dropdown
menu={{
items: [
{ key: 'thirdParty', label: t('application.importWorkflow') },
{ key: 'import', label: t('application.import') },
], onClick: handleClick
}}
placement="bottomRight"
>
<Button>
{t('application.import')}
</Button>
</Dropdown>
<Button type="primary" onClick={handleCreate}>
{t('application.createApplication')}
</Button>
</>}
</Space>
}
</Form>
</Flex>
{(activeTab === 'apps' || activeTab === 'sharing') &&
<PageScrollList<Application, Query>
ref={scrollListRef}
url={getApplicationListUrl}
query={{ ...query, shared_only: activeTab === 'sharing' }}
renderItem={(item) => (
<RbCard
title={item.name}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
>
<Button>
{t('application.import')}
</Button>
</Dropdown>
<Button type="primary" onClick={handleCreate}>
{t('application.createApplication')}
</Button>
</Space>
</Col>
</Row>
<PageScrollList<Application, Query>
ref={scrollListRef}
url={getApplicationListUrl}
query={query}
renderItem={(item) => (
<RbCard
title={item.name}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
>
{['type', 'source', 'created_at'].map((key, index) => (
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
'rb:mt-3': index !== 0
})}>
<span className="rb:text-[#5B6167]">{t(`application.${key}`)}</span>
<span className={clsx({
'rb:text-[#155EEF] rb:font-medium': key === 'type' && item[key] === 'agent',
'rb:text-[#369F21] rb:font-medium': key === 'type' && item[key] === 'multi_agent',
{['type', 'source', 'created_at'].map((key, index) => (
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
'rb:mt-3': index !== 0
})}>
{key === 'source' && item.is_shared
? t('application.shared')
: key === 'source' && !item.is_shared
? t('application.configuration')
: key === 'created_at'
? formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')
: t(`application.${item[key as keyof Application]}`)
}
</span>
</div>
))}
<span className="rb:text-[#5B6167]">{t(`application.${key}`)}</span>
<span className={clsx({
'rb:text-[#155EEF] rb:font-medium': key === 'type' && item[key] === 'agent',
'rb:text-[#369F21] rb:font-medium': key === 'type' && item[key] === 'multi_agent',
})}>
{key === 'source' && item.is_shared
? item.source_workspace_name
: key === 'source' && !item.is_shared
? t('application.configuration')
: key === 'created_at'
? formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')
: t(`application.${item[key as keyof Application]}`)
}
</span>
</div>
))}
<div className="rb:mt-5 rb:flex rb:justify-between rb:gap-2.5">
<Button type="primary" ghost className="rb:w-[calc(100%-46px)]" onClick={() => handleEdit(item)}>{t('application.configuration')}</Button>
<Button icon={<DeleteOutlined />} onClick={() => handleDelete(item)}></Button>
</div>
</RbCard>
)}
/>
{item.is_shared
? <div className="rb:mt-5 rb:flex rb:justify-between rb:gap-2.5">
<Button type="primary" ghost block onClick={() => handleEdit(item)}>{t('common.view')}</Button>
{item.share_permission === 'editable' && <Button type="primary" className="rb:w-[calc(100%-46px)]" onClick={() => handleCopy(item)}>{t('common.copy')}</Button>}
</div>
: <div className="rb:mt-5 rb:flex rb:justify-between rb:gap-2.5">
<Button type="primary" ghost className="rb:w-[calc(100%-46px)]" onClick={() => handleEdit(item)}>{t('application.configuration')}</Button>
<Button icon={<DeleteOutlined />} onClick={() => handleDelete(item)}></Button>
</div>
}
</RbCard>
)}
/>
}
{activeTab === 'myShare' && <MySharing />}
<ApplicationModal
ref={applicationModalRef}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-28 16:16:03
* @Last Modified time: 2026-03-16 09:55:52
*/
/**
* Type definitions for Application Management
@@ -15,6 +15,7 @@ export interface Query {
/** Search keyword */
search: string;
type?: string;
shared_only?: boolean;
}
/**
@@ -53,6 +54,11 @@ export interface Application {
created_at: number;
/** Last update timestamp */
updated_at: number;
share_permission?: string;
source_workspace_name?: string;
source_workspace_icon?: string;
source_app_version?: string;
source_app_is_active?: boolean;
}
/**
@@ -241,4 +247,20 @@ export interface UploadWorkflowModalRef {
export interface UploadModalRef {
/** Open the upload workflow modal */
handleOpen: () => void;
}
export interface MySharedOutItem {
id: string;
source_app_id: string;
source_workspace_id: string;
target_workspace_id: string;
shared_by: string;
permission: 'readonly' | 'editable';
created_at: number;
updated_at: number;
source_app_name: string;
source_app_type: string;
source_app_version: string;
source_app_is_active: boolean;
target_workspace_name: string;
target_workspace_icon: string;
}

View File

@@ -0,0 +1,72 @@
import React, { useState, type FC, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import type { CommunityD3Node, CommunityGraphData, RawCommunityGraphData, RawCommunityNode } from '@/components/D3Graph/types'
import { buildCommunityGraphData } from '@/components/D3Graph/utils'
import CommunityGraph from '@/components/D3Graph/CommunityGraph'
import { getMemoryCommunityGraph } from '@/api/memory'
// ─── Tooltip ──────────────────────────────────────────────────────────────────
const NodeTooltip: FC<{ node: CommunityD3Node }> = ({ node }) => {
const { t } = useTranslation()
return (
<div style={{
background: '#fff', border: '1px solid #DFE4ED', borderRadius: 8,
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: '10px 14px',
minWidth: 180, maxWidth: 260, fontSize: 13,
}}>
<div style={{ fontWeight: 600, marginBottom: 6, color: '#1a1a1a', fontSize: 14 }}>
{node.properties?.name ?? node.name}
</div>
{node.properties?.description && (
<div style={{ color: '#5B6167', lineHeight: '20px', marginBottom: 4 }}>
{node.properties.description}
</div>
)}
<div style={{ color: '#5B6167', lineHeight: '22px' }}>
{t('userMemory.type')}
<span style={{ color: '#1a1a1a' }}>{t(`userMemory.${node.properties?.entity_type}`)}</span>
</div>
<div style={{ color: '#5B6167', lineHeight: '22px' }}>
{t('userMemory.community')}
<span style={{ color: node.color, fontWeight: 500 }}>{node.properties?.community_name}</span>
</div>
</div>
)
}
// ─── Component ────────────────────────────────────────────────────────────────
const CommunityNetwork: FC<{ onSelectCommunity?: (node: RawCommunityNode) => void }> = ({ onSelectCommunity }) => {
const { id } = useParams()
const [graphData, setGraphData] = useState<CommunityGraphData | null>(null)
const [empty, setEmpty] = useState(false)
useEffect(() => {
if (!id) return
const controller = new AbortController()
setEmpty(false)
setGraphData(null)
getMemoryCommunityGraph(id, { signal: controller.signal }).then(res => {
const raw = res as RawCommunityGraphData
if (!raw.nodes?.length) { setEmpty(true); return }
const built = buildCommunityGraphData(raw)
if (!built) { setEmpty(true); return }
setGraphData(built)
}).catch((e) => { if (e?.code !== 'ERR_CANCELED') setEmpty(true) })
return () => controller.abort()
}, [id])
return (
<CommunityGraph
data={graphData}
empty={empty}
showLegend={false}
onCommunityClick={onSelectCommunity}
renderTooltip={node => <NodeTooltip node={node} />}
/>
)
}
export default React.memo(CommunityNetwork)

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 18:32:00
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 18:32:00
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 14:51:17
*/
/**
* Relationship Network Component
@@ -13,18 +13,20 @@
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams, useNavigate } from 'react-router-dom'
import { Col, Row, Space, Button } from 'antd'
import { Col, Row, Space, Button, Tabs, Flex, Divider } from 'antd'
import dayjs from 'dayjs'
import ReactEcharts from 'echarts-for-react'
import RbCard from '@/components/RbCard/Card'
import detailEmpty from '@/assets/images/userMemory/detail_empty.png'
import type { Node, Edge, GraphData, StatementNodeProperties, ExtractedEntityNodeProperties } from '../types'
import type { RawCommunityNode } from '@/components/D3Graph/types'
import {
getMemorySearchEdges,
} from '@/api/memory'
import Empty from '@/components/Empty'
import Tag from '@/components/Tag'
import CommunityNetwork from './CommunityNetwork'
/** Node color palette */
const colors = ['#155EEF', '#369F21', '#4DA8FF', '#FF5D34', '#9C6FFF', '#FF8A4C', '#8BAEF7', '#FFB048']
@@ -36,16 +38,21 @@ const RelationshipNetwork:FC = () => {
const [nodes, setNodes] = useState<Node[]>([])
const [links, setLinks] = useState<Edge[]>([])
const [categories, setCategories] = useState<{ name: string }[]>([])
const [selectedNode, setSelectedNode] = useState<Node | null>(null)
const [selectedNode, setSelectedNode] = useState<Node | RawCommunityNode | null>(null)
// const [fullScreen, setFullScreen] = useState<boolean>(false)
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState('relationshipNetwork')
console.log('categories', categories)
const edgeAbortRef = useRef<AbortController | null>(null)
/** Fetch relationship network data */
const getEdgeData = useCallback(() => {
if (!id) return
edgeAbortRef.current?.abort()
edgeAbortRef.current = new AbortController()
setSelectedNode(null)
getMemorySearchEdges(id).then((res) => {
getMemorySearchEdges(id, { signal: edgeAbortRef.current.signal }).then((res) => {
const { nodes, edges, statistics } = res as GraphData
const curNodes: Node[] = []
const curEdges: Edge[] = []
@@ -123,6 +130,7 @@ const RelationshipNetwork:FC = () => {
useEffect(() => {
if (!id) return
getEdgeData()
return () => { edgeAbortRef.current?.abort() }
}, [id])
useEffect(() => {
@@ -153,34 +161,36 @@ const RelationshipNetwork:FC = () => {
const params = new URLSearchParams({
nodeId: selectedNode.id,
nodeLabel: selectedNode.label,
nodeName: selectedNode.name || ''
nodeName: (selectedNode as Node).name || ''
})
navigate(`/user-memory/detail/${id}/GRAPH?${params.toString()}`)
}
const handleChangeTab = (tab: string) => {
if (tab === 'communityNetwork') {
edgeAbortRef.current?.abort()
} else {
getEdgeData()
}
setActiveTab(tab)
setSelectedNode(null)
}
return (
<Row gutter={16}>
{/* Relationship Network */}
<Col span={16}>
<RbCard
title={t('userMemory.relationshipNetwork')}
headerType="borderless"
headerClassName="rb:min-h-[46px]!"
// extra={
// <div
// onClick={handleFullScreen}
// className="rb:group rb:cursor-pointer rb:hover:text-[#212332] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:flex rb:items-center rb:gap-1"
// >
// <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/fullScreen.svg')] rb:hover:bg-[url('@/assets/images/fullScreen_hover.svg')]"></div>
// {t('userMemory.fullScreen')}
// </div>
// }
>
<RbCard bodyClassName="rb:pt-0!">
<Tabs
items={['relationshipNetwork', 'communityNetwork'].map(key => ({ key, label: t(`userMemory.${key}`) }))}
activeKey={activeTab}
onChange={handleChangeTab}
/>
<div className="rb:h-129.5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm">
{nodes.length === 0 ? (
<Empty className="rb:h-full" />
) : (
<ReactEcharts
{activeTab === 'communityNetwork'
? <CommunityNetwork onSelectCommunity={community => setSelectedNode(community)} />
: nodes.length === 0
? <Empty className="rb:h-full" />
: <ReactEcharts
option={{
colors: colors,
tooltip: {
@@ -253,103 +263,121 @@ const RelationshipNetwork:FC = () => {
}
}}
/>
)}
}
</div>
</RbCard>
</Col>
{/* Memory Details */}
<Col span={8}>
<RbCard
<RbCard
title={t('userMemory.memoryDetails')}
headerType="borderless"
headerClassName="rb:min-h-[46px]!"
bodyClassName='rb:p-0!'
extra={selectedNode && <Button type="text" onClick={handleViewAll}>
<div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/view.svg')] rb:hover:bg-[url('@/assets/images/userMemory/view_hover.svg')]"
></div>
{t('userMemory.completeMemory')}
</Button>}
bodyClassName="rb:p-0!"
extra={selectedNode && !(selectedNode as RawCommunityNode).properties.community_id && (
<Button type="text" onClick={handleViewAll}>
<div className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/view.svg')] rb:hover:bg-[url('@/assets/images/userMemory/view_hover.svg')]" />
{t('userMemory.completeMemory')}
</Button>
)}
>
<div className="rb:h-133.5 rb:overflow-y-auto">
{!selectedNode
? <Empty
url={detailEmpty}
subTitle={t('userMemory.memoryDetailEmptyDesc')}
className="rb:h-full rb:mx-10 rb:text-center"
size={[197.81, 150]}
/>
: <>
{selectedNode.name && <div className="rb:bg-[#F6F8FC] rb:border-t rb:border-b rb:border-[#DFE4ED] rb:font-medium rb:py-2 rb:px-4 rb:h-10">{selectedNode.name}</div>}
<div className="rb:p-4">
<>
? <Empty url={detailEmpty} subTitle={activeTab === 'relationshipNetwork' ? t('userMemory.memoryDetailEmptyDesc') : t('userMemory.communityDetailEmptyDesc')} className="rb:h-full rb:mx-10 rb:text-center" size={[197.81, 150]} />
: (selectedNode as RawCommunityNode).properties.community_id
? <div className="rb:p-3 rb:pt-0">
<div className="rb:font-medium rb:text-[#212332] rb:text-[16px] rb:leading-5.5 rb:pl-1">
{(selectedNode as RawCommunityNode).properties.name}
</div>
<div className="rb:mt-3 rb:font-medium rb:leading-5 rb:pl-1">{t('userMemory.summary')}</div>
<div className="rb:bg-[#F6F6F6] rb:rounded-xl rb:px-3 rb:py-2.5 rb:mt-2">
{(selectedNode as RawCommunityNode).properties.summary}
</div>
<Flex align="center" justify="space-between" className="rb:mt-5!">
<span className="rb:text-[#5B6167] rb:font-regular rb:pl-1">{t('userMemory.member_count')}</span>
<span className="rb:font-medium">{(selectedNode as RawCommunityNode).properties.member_count}{t('userMemory.member_count_desc')}</span>
</Flex>
<Divider className='rb:my-2.5!' />
<div className="rb:font-medium rb:leading-5 rb:pl-1">{t('userMemory.core_entities')}</div>
<ul className="rb:list-disc rb:pl-4 rb:text-[#5B6167] rb:mt-2">
{(selectedNode as RawCommunityNode).properties.core_entities.map((entity, index) => <li key={index}>{entity}</li>)}
</ul>
</div>
: <>
{(selectedNode as Node).name && (
<div className="rb:bg-[#F6F8FC] rb:border-t rb:border-b rb:border-[#DFE4ED] rb:font-medium rb:py-2 rb:px-4 rb:h-10">
{(selectedNode as Node).name}
</div>
)}
<div className="rb:p-4">
<div className="rb:font-medium rb:leading-5">{t('userMemory.memoryContent')}</div>
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
{['Chunk', 'Dialogue', 'MemorySummary'].includes(selectedNode.label) && 'content' in selectedNode.properties
? selectedNode.properties.content
: selectedNode.label === 'ExtractedEntity' && 'description' in selectedNode.properties
? selectedNode.properties.description
: selectedNode.label === 'Statement' && 'statement' in selectedNode.properties
? selectedNode.properties.statement
: ''
}
</div>
</>
<div className="rb:font-medium rb:mb-2 rb:mt-4">
<div className="rb:font-medium rb:leading-5">{t('userMemory.created_at')}</div>
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
{dayjs(selectedNode?.properties.created_at).format('YYYY-MM-DD HH:mm:ss')}
? selectedNode.properties.description
: selectedNode.label === 'Statement' && 'statement' in selectedNode.properties
? selectedNode.properties.statement
: ''}
</div>
{selectedNode?.properties.associative_memory > 0 && <div className="rb:mt-4">
<div className="rb:font-medium rb:leading-5">{t('userMemory.associative_memory')}</div>
<div className="rb:font-medium rb:mb-2 rb:mt-4">
<div className="rb:font-medium rb:leading-5">{t('userMemory.created_at')}</div>
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
<span className="rb:text-[#155EEF] rb:font-medium">{selectedNode?.properties.associative_memory}</span> {t('userMemory.unix')}{t('userMemory.associative_memory')}
{dayjs((selectedNode as Node).properties.created_at).format('YYYY-MM-DD HH:mm:ss')}
</div>
</div>}
{selectedNode.label === 'Statement' && <>
{(['emotion_keywords', 'emotion_type', 'emotion_subject', 'importance_score'] as const).map(key => {
const statementProps = selectedNode.properties as StatementNodeProperties;
if ((key === 'emotion_keywords' && statementProps[key]?.length > 0) || typeof statementProps[key] === 'string') {
console.log('statementProps[key]', statementProps[key])
return (
<div className="rb:mt-4" key={key}>
{t(`userMemory.Statement_${key}`)}
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
{key === 'emotion_keywords'
? <Space>{statementProps.emotion_keywords.map((vo, index) => <Tag key={index}>{vo}</Tag>)}</Space>
: statementProps[key]
}
{(selectedNode as Node).properties.associative_memory > 0 && (
<div className="rb:mt-4">
<div className="rb:font-medium rb:leading-5">{t('userMemory.associative_memory')}</div>
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
<span className="rb:text-[#155EEF] rb:font-medium">{(selectedNode as Node).properties.associative_memory}</span>
{' '}{t('userMemory.unix')}{t('userMemory.associative_memory')}
</div>
</div>
)}
{selectedNode.label === 'Statement' && (
(['emotion_keywords', 'emotion_type', 'emotion_subject', 'importance_score'] as const).map(key => {
const p = selectedNode.properties as StatementNodeProperties
if ((key === 'emotion_keywords' && p[key]?.length > 0) || typeof p[key] === 'string') {
return (
<div className="rb:mt-4" key={key}>
{t(`userMemory.Statement_${key}`)}
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
{key === 'emotion_keywords'
? <Space>{p.emotion_keywords.map((v, i) => <Tag key={i}>{v}</Tag>)}</Space>
: p[key]}
</div>
</div>
</div>
)
}
return null
})}
</>}
{selectedNode.label === 'ExtractedEntity' && <>
{(['name', 'entity_type', 'aliases', 'connect_strngth', 'importance_score'] as const).map(key => {
const entityProps = selectedNode.properties as ExtractedEntityNodeProperties;
if (entityProps[key]) {
return (
<div className="rb:mt-4" key={key}>
{t(`userMemory.ExtractedEntity_${key}`)}
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
{Array.isArray(entityProps[key]) && entityProps[key].length > 0
? entityProps[key].map((vo, index) => <div key={index}>- {vo}</div>)
: entityProps[key]
}
)
}
return null
})
)}
{selectedNode.label === 'ExtractedEntity' && (
(['name', 'entity_type', 'aliases', 'connect_strngth', 'importance_score'] as const).map(key => {
const p = selectedNode.properties as ExtractedEntityNodeProperties
if (p[key]) {
return (
<div className="rb:mt-4" key={key}>
{t(`userMemory.ExtractedEntity_${key}`)}
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
{Array.isArray(p[key]) && p[key].length > 0
? p[key].map((v, i) => <div key={i}>- {v}</div>)
: p[key]}
</div>
</div>
</div>
)
}
return null
})}
</>}
)
}
return null
})
)}
</div>
</div>
</div>
</>
</>
}
</div>
</RbCard>

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:57:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:57:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 11:49:52
*/
/**
* User Memory Detail Types
@@ -90,6 +90,7 @@ export interface ExtractedEntityNodeProperties {
connect_strngth: string;
importance_score: number;
associative_memory: number;
community_name?: string;
}
/**
* Memory summary node
@@ -246,4 +247,53 @@ export interface ForgetData {
*/
export interface GraphDetailRef {
handleOpen: (vo: Node) => void
}
}
// Community
export type CommunityNodeType = 'Community' | 'ExtractedEntity';
export type CommunityEdgeType = 'BELONGS_TO_COMMUNITY' | 'EXTRACTED_RELATIONSHIP';
export type CommunityEntityType = "Person" | "Organization" | "ORG" | "Location" | "LOC" | "Event" | "Concept" | "Time" | "Position" | "WorkRole" | "System" | "Policy" | "HistoricalPeriod" | "HistoricalState" | "HistoricalEvent" | "EconomicFactor" | "Condition" | "Numeric" | "Work";
// 社区节点
export interface CommunityTypeNode {
id: string;
label: 'Community';
properties: {
community_id: string;
end_user_id: string;
member_count: number;
updated_at: string;
name: string;
summary: string;
core_entities: string[];
member_entity_ids: string[];
};
}
// 核心实体
export interface ExtractedEntityTypeNode {
id: string;
label: 'ExtractedEntity';
properties: {
name: string;
end_user_id: string;
description: string;
created_at: string;
entity_type: CommunityEntityType;
community_name: string;
};
}
// 社区图谱连线
export interface CommunityEdge {
id: string;
target: string;
source: string;
}
export interface CommunityStatistics {
total_nodes: number;
total_edges: number;
node_types: Record<CommunityNodeType, number>;
edge_types: Record<CommunityEdgeType, number>;
}
export interface CommunityGraphData {
nodes: (CommunityTypeNode | ExtractedEntityTypeNode)[];
edges: CommunityEdge[];
statistics: CommunityStatistics;
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-24 17:57:08
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-28 16:48:09
* @Last Modified time: 2026-03-12 13:39:24
*/
/*
* Runtime Component
@@ -225,12 +225,13 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
</div>
)
: <>
{item.error
? <div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 ", getStatus('failed'))}>
{item.error &&
<div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 rb:mb-2 rb:-mt-4", getStatus('failed'))}>
<Markdown content={item.error} />
</div>
: renderChild(item.subContent)
}</>
</div>
}
{renderChild(item.subContent)}
</>
)
}]}
/>

View File

@@ -8,7 +8,6 @@ import RbModal from '@/components/RbModal'
interface VariableEditModalProps {
refresh: (values: Variable[]) => void;
variables: Variable[]
}
const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModalProps>(({

View File

@@ -60,7 +60,8 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
handleRun,
graphRef,
addVariable,
config
config,
funConfig: config?.funConfig
}))
return (
<div className="rb:h-[calc(100vh-64px)] rb:relative">

View File

@@ -2,6 +2,7 @@
import { Graph } from '@antv/x6';
import type { KnowledgeConfig } from './components/Properties/Knowledge/types'
import type { Variable } from './components/Properties/VariableList/types'
import type { FunConfigForm } from '@/views/ApplicationConfig/types'
export interface NodeConfig {
type: 'input' | 'textarea' | 'select' | 'inputNumber' | 'slider' | 'customSelect' | 'define' | 'knowledge' | 'variableList' | string;
placeholder?: string;
@@ -89,6 +90,8 @@ export interface WorkflowConfig {
is_active: boolean;
created_at: number;
updated_at: number;
funConfig?: FunConfigForm;
}
export interface ChatRef {