feat(web): app page ui upgrade

This commit is contained in:
zhaoying
2026-03-07 13:46:08 +08:00
parent e2b6c713e7
commit 06fe3f2f01
29 changed files with 938 additions and 961 deletions

View File

@@ -311,4 +311,7 @@ body {
.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
color: #212332;
background: #F6F6F6;
}
.ant-checkbox .ant-checkbox-inner {
border-radius: 6px !important;
}

View File

@@ -2,13 +2,12 @@
* @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-04 10:28:59
*/
import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx'
import { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom';
import { Row, Col, Space, Form, Input, Switch, Button, App, Spin } from 'antd'
import { Row, Col, Space, Form, Input, Button, App, Spin, Flex } from 'antd'
import Chat from './components/Chat'
import RbCard from '@/components/RbCard/Card'
@@ -35,92 +34,13 @@ import VariableList from './components/VariableList/VariableList'
import { getApplicationConfig } from '@/api/application'
import { memoryConfigListUrl } from '@/api/memory'
import CustomSelect from '@/components/CustomSelect'
import aiPrompt from '@/assets/images/application/aiPrompt.png'
import AiPromptModal from './components/AiPromptModal'
import ToolList from './components/ToolList/ToolList'
import SkillList from './components/Skill'
import ChatVariableConfigModal from './components/ChatVariableConfigModal';
import type { Skill } from '@/views/Skills/types'
/**
* Description wrapper component
* @param desc - Description text
* @param className - Additional CSS classes
*/
const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => {
return (
<div className={clsx(className, "rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>
{desc}
</div>
)
}
/**
* Label wrapper component
* @param title - Label title
* @param className - Additional CSS classes
* @param children - Child elements
*/
const LabelWrapper: FC<{title: string, className?: string; children?: ReactNode}> = ({title, className, children}) => {
return (
<div className={clsx(className, "rb:text-[14px] rb:font-medium rb:leading-5")}>
{title}
{children}
</div>
)
}
/**
* Switch wrapper component with label and description
* @param title - Switch title
* @param desc - Optional description
* @param name - Form field name
* @param needTransition - Whether to translate text
*/
const SwitchWrapper: FC<{ title: string, desc?: string, name: string | string[]; needTransition?: boolean; }> = ({ title, desc, name, needTransition = true }) => {
const { t } = useTranslation();
return (
<div className="rb:flex rb:items-center rb:justify-between">
<LabelWrapper title={needTransition ? t(`application.${title}`) : title}>
{desc && <DescWrapper desc={needTransition ? t(`application.${desc}`) : desc} className="rb:mt-2" />}
</LabelWrapper>
<Form.Item
name={name}
valuePropName="checked"
className="rb:mb-0!"
>
<Switch />
</Form.Item>
</div>
)
}
/**
* Select wrapper component with label and description
* @param title - Select title
* @param desc - Description text
* @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 { t } = useTranslation();
return (
<>
<LabelWrapper title={t(`application.${title}`)} className="rb:mb-2">
</LabelWrapper>
<Form.Item
name={name}
className="rb:mb-0!"
>
<CustomSelect
placeholder={t('common.pleaseSelect')}
url={url}
hasAll={false}
valueKey='config_id'
labelKey="config_name"
/>
</Form.Item>
<DescWrapper desc={t(`application.${desc}`)} className="rb:mt-2" />
</>
)
}
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
import DescWrapper from '@/components/FormItem/DescWrapper'
/**
* Agent configuration component
@@ -172,8 +92,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
const allSkills = Array.isArray(skills?.skill_ids) ? skills?.skill_ids.map(vo => ({ id: vo })) : []
const allTools = Array.isArray(response.tools) ? response.tools : []
const memoryContent = response.memory?.memory_config_id
const parsedMemoryContent = memoryContent === null || memoryContent === ''
? undefined
const parsedMemoryContent = memoryContent === null || memoryContent === ''
? undefined
: !isNaN(Number(memoryContent)) ? Number(memoryContent) : memoryContent
const variableList = variables?.map((item, index) => ({
...item,
@@ -410,46 +330,48 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
return (
<>
{loading && <Spin fullscreen></Spin>}
<Row className="rb:h-[calc(100vh-64px)]">
<Col span={12} className="rb:h-full rb:overflow-x-auto rb:border-r rb:border-[#DFE4ED] rb:p-[20px_16px_24px_16px]">
<div className="rb:flex rb:items-center rb:justify-end rb:mb-5">
<Space size={10}>
<Button onClick={handleModelConfig} className="rb:group">
{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>
<Button type="primary" onClick={() => handleSave()}>
{t('common.save')}
</Button>
</Space>
</div>
<Row className="rb:h-[calc(100vh-88px)]" gutter={12}>
<Col span={12} className="rb:h-full rb:overflow-y-auto">
<Form form={form}>
<Form.Item name="default_model_config_id" hidden noStyle></Form.Item>
<Form.Item name="model_parameters" 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">
<div className="rb:font-medium rb:leading-5">
{t('application.configuration')}
<span className="rb:font-regular rb:text-[12px] rb:text-[#5B6167]"> ({t('application.configurationDesc')})</span>
</div>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handlePrompt}>
<img src={aiPrompt} className="rb:size-5" />
{t('application.aiPrompt')}
</Button>
<Flex gap={16} vertical>
<Flex align="center" justify="space-between" className="rb:p-3! rb:bg-white rb:rounded-xl">
<Button type="primary" ghost onClick={handleModelConfig} className="rb:group">
{defaultModel?.name ? <div className="rb:size-4 rb:bg-[url('@/assets/images/application/model.svg')]"></div> : null}
{defaultModel?.name || t('application.chooseModel')}
</Button>
<Button type="primary" onClick={() => handleSave()}>
{t('common.save')}
</Button>
</Flex>
<Form.Item name="default_model_config_id" hidden noStyle></Form.Item>
<Form.Item name="model_parameters" hidden noStyle></Form.Item>
<Card
title={t('application.promptConfiguration')}
extra={
<Space
size={1}
className="rb:px-2 rb:h-5.5 rb:rounded-md rb:cursor-pointer rb:border rb:border-[rgba(21,94,239,0.3)] rb:text-[#155EEF]"
onClick={handlePrompt}
>
<div className="rb:size-5 rb:bg-cover rb:bg-[url('@/assets/images/application/aiPrompt.png')]"></div>
<span className="rb:font-[PingFangSC, PingFang_SC]!">{t('application.aiPrompt')}</span>
</Space>
}
>
<div className="rb:leading-4.5 rb:text-[12px] rb:mb-2">
<span className="rb:font-medium">{t('application.configuration')}</span>
<span className="rb:font-regular rb:text-[#5B6167]"> ({t('application.configurationDesc')})</span>
</div>
<Form.Item
name="system_prompt"
className="rb:mb-0!"
rules={[{ max: 10000 }]}
>
<Form.Item name="system_prompt" className="rb:mb-0!">
<Input.TextArea
placeholder={t('application.promptPlaceholder')}
styles={{
textarea: {
minHeight: '200px',
borderRadius: '8px'
borderRadius: '8px',
padding: '12px'
},
}}
/>
@@ -462,15 +384,28 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
{/* Memory Configuration */}
<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"
name={['memory', 'memory_config_id']}
url={memoryConfigListUrl}
<Flex gap={16} vertical className="rb:bg-[#FAFAFA] rb:rounded-xl rb:p-3!">
<SwitchFormItem
title={t('application.dialogueHistoricalMemory')}
name={['memory', 'enabled']}
desc={t('application.dialogueHistoricalMemoryDesc')}
/>
</Space>
<Form.Item
name={['memory', 'memory_config_id']}
label={t('application.selectMemoryContent')}
extra={<DescWrapper desc={t('application.selectMemoryContentDesc')} className="rb:mt-1" />}
layout="vertical"
className="rb:mb-0!"
>
<CustomSelect
placeholder={t('common.pleaseSelect')}
url={memoryConfigListUrl}
hasAll={false}
valueKey='config_id'
labelKey="config_name"
/>
</Form.Item>
</Flex>
</Card>
<Form.Item name="variables" noStyle>
@@ -485,26 +420,29 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
<Form.Item name="tools" noStyle>
<ToolList />
</Form.Item>
</Space>
</Flex>
</Form>
</Col>
<Col span={12} className="rb:h-full rb:overflow-x-hidden rb:p-[20px_16px_24px_16px]">
<div className="rb:flex rb:items-center rb:justify-between rb:mb-5">
{t('application.debuggingAndPreview')}
<Space size={10}>
{chatVariables.length > 0 &&
<Col span={12} className="rb:h-full rb:overflow-y-hidden">
<RbCard
title={t('application.debuggingAndPreview')}
extra={
<Space size={10}>
<Button type="primary" ghost onClick={handleOpenVariableConfig}>
{t('application.variableConfig')}
</Button>
}
<Button type="primary" ghost onClick={handleAddModel}>
+ {t('application.addModel')}
</Button>
<div className="rb:w-8 rb:h-8 rb:cursor-pointer rb:bg-[url('@/assets/images/application/clean.svg')]" onClick={handleClearDebugging}></div>
</Space>
</div>
<RbCard height="calc(100vh - 160px)" bodyClassName="rb:p-[0]! rb:h-full rb:overflow-hidden">
<Button type="primary" ghost onClick={handleAddModel}>
+ {t('application.addModel')}
</Button>
<div className="rb:w-8 rb:h-8 rb:cursor-pointer rb:bg-[url('@/assets/images/application/clean.svg')]" onClick={handleClearDebugging}></div>
</Space>
}
headerType="borderless"
headerClassName="rb:h-[56px]! rb:leading-[22px]!"
titleClassName="rb:font-[MiSans-Bold] rb:font-bold"
bodyClassName="rb:p-4! rb:pt-0!"
className="rb:h-full"
>
<Chat
data={data as Config}
chatList={chatList}

View File

@@ -1,24 +1,23 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:29
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:29
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 19:05:23
*/
import { type FC, useState, useRef, useEffect } from 'react';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import { Button, Space, App, Statistic, Row, Col } from 'antd';
import { Button, Space, App, Row, Col, Flex } from 'antd';
import copy from 'copy-to-clipboard'
import Card from './components/Card';
import type { Application } from '@/views/ApplicationManagement/types'
import type { ApiKeyModalRef, ApiKeyConfigModalRef } from './types'
import type { ApiKey } from '@/views/ApiKeyManagement/types'
import ApiKeyModal from './components/ApiKeyModal';
import ApiKeyConfigModal from './components/ApiKeyConfigModal';
import Tag from '@/components/Tag'
import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey';
import { maskApiKeys } from '@/utils/apiKeyReplacer'
import RbCard from '@/components/RbCard/Card';
/**
* API configuration page component
@@ -125,61 +124,69 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
// Calculate total requests across all API keys
const totalRequests = apiKeyList.reduce((total, item) => total + item.total_requests, 0);
return (
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
<Space size={20} direction="vertical" style={{width: '100%'}}>
<Card
title={t('application.endpointConfiguration')}
<div className="rb:w-250 rb:mx-auto">
<Flex gap={20} vertical>
<RbCard
title={<Flex align="center">
{t('application.endpointConfiguration')}
<span className="rb:text-[#5B6167] rb:text-[12px]">({t('application.endpointConfigurationSubTitle')})</span>
</Flex>}
headerType="borderless"
headerClassName="rb:min-h-13.5!"
>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.endpointConfigurationSubTitle')}</div>
<div className="rb:p-[20px_20px_24px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<Space size={8}>
{['GET', 'POST', 'PUT', 'DELETE'].map((method) => (
<div key={method} className={clsx("rb:w-20 rb:h-7 rb:leading-7 rb:text-center rb:rounded-md rb:text-regular", {
'rb:bg-[#155EEF] rb:text-white': activeMethods.includes(method),
'rb:bg-white': !activeMethods.includes(method),
})}>
{method}
</div>
))}
</Space>
<Space size={8}>
{['GET', 'POST', 'PUT', 'DELETE'].map((method) => (
<div key={method} className={clsx("rb:w-20 rb:h-7 rb:leading-7 rb:text-center rb:rounded-md rb:text-regular", {
'rb:bg-[#171719] rb:text-white': activeMethods.includes(method),
'rb:bg-white rb:border rb:border-[#EBEBEB] rb:text-[#212332]': !activeMethods.includes(method),
})}>
{method}
</div>
))}
</Space>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[20px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
{copyContent}
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(copyContent)}>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
</div>
</div>
</Card>
<Card
title={t('application.apiKeys')}
<Flex align="center" justify="space-between" className="rb:text-[#5B6167] rb:mt-4! rb:py-5! rb:px-4! rb:bg-white rb-border rb:rounded-lg rb:leading-5">
{copyContent}
<Button className="rb:px-2! rb:h-7! rb:group rb:-mt-1.75!" onClick={() => handleCopy(copyContent)}>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
</Flex>
</RbCard>
<RbCard
title={<Flex align="center">
{t('application.apiKeys')}
<span className="rb:text-[#5B6167] rb:text-[12px]">({t('application.apiKeySubTitle')})</span>
</Flex>}
extra={
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAdd}>+ {t('application.addApiKey')}</Button>
}
headerType="borderless"
headerClassName="rb:min-h-13.5!"
>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.apiKeySubTitle')}</div>
{/* Overview Data */}
<Row>
<Row className="rb:pl-1 rb:mb-4">
<Col span={6}>
<Statistic title={t('application.apiKeyTotal')} value={apiKeyList.length} />
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[20px] rb:leading-7">{apiKeyList.length}</div>
<div className="rb:mt-1 rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">{t('application.apiKeyTotal')}</div>
</Col>
<Col span={6}>
<Statistic title={t('application.apiKeyRequestTotal')} value={totalRequests} />
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[20px] rb:leading-7">{totalRequests}</div>
<div className="rb:mt-1 rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">{t('application.apiKeyRequestTotal')}</div>
</Col>
</Row>
{/* API Key List */}
{apiKeyList.sort((a, b) => b.created_at - a.created_at).map(item => (
<div key={item.id} className="rb:mt-4 rb:p-[10px_12px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<div className="rb:flex rb:items-center rb:justify-between">
<div className="rb:flex rb:items-center rb:max-w-[calc(100%-92px)]">
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1">{item.name}</div>
<Tag className="rb:ml-2">ID: {item.id}</Tag>
</div>
<Space>
<div key={item.id} className="rb:p-4 rb-border rb:rounded-xl">
<Flex align="center" justify="space-between">
<Flex vertical className="rb:max-w-[calc(100%-92px)]" gap={4}>
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1 rb:leading-5 rb:font-medium">{item.name}</div>
<div className="rb:text-[#5B6167] rb:leading-4.5">ID: {item.id}</div>
</Flex>
<Space size={12}>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleEdit(item)}
@@ -189,30 +196,38 @@ const Api: FC<{ application: Application | null }> = ({ application }) => {
onClick={() => handleDelete(item)}
></div>
</Space>
</div>
<div className="rb:mb-3 rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
{maskApiKeys(item.api_key)}
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(item.api_key)}>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
</div>
<Row gutter={12}>
</Flex>
<Row className="rb:mt-4">
<Col span={8}>
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.apiKeyRequestTotal')} value={item.total_requests} />
<Row className="rb:px-4 rb:py-2">
<Col span={12}>
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[16px] rb:leading-5.5">{item.total_requests}</div>
<div className="rb:mt-1 rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">{t('application.apiKeyRequestTotal')}</div>
</Col>
<Col span={12}>
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[16px] rb:leading-5.5">{item.rate_limit}</div>
<div className="rb:mt-1 rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">{t('application.qpsLimit')}</div>
</Col>
</Row>
</Col>
<Col span={8}>
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.qpsLimit')} value={item.rate_limit} />
<Col span={16}>
<Flex align="center" justify="space-between" className="rb:text-[#5B6167] rb:py-5! rb:px-4! rb:bg-white rb-border rb:rounded-lg rb:leading-5">
{maskApiKeys(item.api_key)}
<Button className="rb:px-2! rb:h-7! rb:group rb:-mt-1.75!" onClick={() => handleCopy(item.api_key)}>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
</Flex>
</Col>
</Row>
</div>
))}
</Card>
</Space>
</RbCard>
</Flex>
<ApiKeyModal
ref={apiKeyModalRef}

View File

@@ -1,16 +1,16 @@
/*
* @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-04 10:20:16
*/
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom';
import { Form, Space, Row, Col, Button, Flex, App, Select } from 'antd'
import { Form, Space, Row, Col, Button, Flex, App, Select, Spin } from 'antd'
import Card from './components/Card'
import Tag, { type TagProps } from './components/Tag'
import Tag from './components/Tag'
import CustomSelect from '@/components/CustomSelect';
import { getMultiAgentConfig, saveMultiAgentConfig, getApplicationList } from '@/api/application';
import type {
@@ -30,8 +30,6 @@ import { getModelListUrl } from '@/api/models'
import ModelConfigModal from './components/ModelConfigModal'
import type { Application } from '@/views/ApplicationManagement/types'
const tagColors = ['processing', 'warning', 'default']
const MAX_LENGTH = 5;
/**
* Multi-agent cluster configuration component
@@ -51,6 +49,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
list: []
},
])
const [loading, setLoading] = useState(false)
/**
* Save cluster configuration
@@ -103,6 +102,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
if (!id) {
return
}
setLoading(true)
getMultiAgentConfig(id as string).then(res => {
const response = res as Config
setData(response)
@@ -131,6 +131,9 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
setSubAgents(sub_agents)
}
})
.finally(() => {
setLoading(false)
})
}
/**
* Open sub-agent modal for add or edit
@@ -186,80 +189,84 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
}
return (
<Row className="rb:h-[calc(100vh-64px)]">
<Col span={12} className="rb:h-full rb:overflow-x-auto rb:border-r rb:border-[#DFE4ED] rb:p-[20px_16px_24px_16px]">
<div className="rb:flex rb:items-center rb:justify-end rb:mb-5">
<Button type="primary" onClick={() => handleSave()}>
{t('common.save')}
</Button>
</div>
<Form form={form} layout="vertical">
<Space size={20} direction="vertical" style={{width: '100%'}}>
<Card title={t('application.collaboration')}>
<Form.Item
name="orchestration_mode"
noStyle
>
<RadioGroupCard
options={['supervisor', 'collaboration'].map((type) => ({
value: type,
label: t(`application.${type}`),
labelDesc: t(`application.${type}Desc`),
}))}
allowClear={false}
/>
</Form.Item>
</Card>
<Card title={t('application.subAgentsManagement')}>
<Flex align="center" justify="space-between">
<div className="rb:font-regular rb:text-[#5B6167] rb:leading-5">{t('application.added')}: {subAgents.length}/{MAX_LENGTH}</div>
<Button size="small" disabled={subAgents.length >= MAX_LENGTH} onClick={() => handleSubAgentModal()}>{t('application.addSubAgent')}</Button>
<>
{loading && <Spin fullscreen></Spin>}
<Row className="rb:h-[calc(100vh-88px)]" gutter={12}>
<Col span={12} className="rb:h-full rb:overflow-x-auto rb:border-r rb:border-[#DFE4ED]">
<Form form={form} layout="vertical">
<Flex gap={16} vertical>
<Flex align="center" justify="end" className="rb:p-3! rb:bg-white rb:rounded-xl">
<Button type="primary" onClick={() => handleSave()}>
{t('common.save')}
</Button>
</Flex>
<Card title={t('application.collaboration')}>
<Form.Item
name="orchestration_mode"
noStyle
>
<RadioGroupCard
options={['supervisor', 'collaboration'].map((type) => ({
value: type,
label: t(`application.${type}`),
labelDesc: t(`application.${type}Desc`),
}))}
allowClear={false}
block={true}
/>
</Form.Item>
</Card>
{subAgents.length === 0
? <Empty size={88} />
: subAgents.map((agent, index) => (
<Flex key={index} align="center" justify="space-between"
className="rb:mt-4! rb:w-full! rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-[20px_31px_20px_20px]!"
>
<Flex className="rb:w-[calc(100%-80px)]!">
<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]">
{agent.name?.[0]}
</div>
<div className="rb:flex rb:flex-col rb:justify-center rb:max-w-[calc(100%-60px)]">
<div>{agent.name}
<Tag color={agent.is_active ? 'success' : 'warning'} className="rb:ml-2">
{agent.is_active ? t('common.enable') : t('common.deleted')}
</Tag>
</div>
{agent.role && <div className="rb:font-regular rb:leading-5 rb:text-[#5B6167] rb:mt-1.5">{agent.role || '-'}</div>}
{agent.capabilities && <Flex wrap gap={8} className="rb:mt-4">{agent.capabilities.map((tag, tagIndex) => <Tag key={tagIndex} color={tagColors[tagIndex % tagColors.length] as TagProps['color']}>{tag}</Tag>)}</Flex>}
</div>
</Flex>
<Space>
<div
className="rb:w-8 rb:h-8 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleSubAgentModal(agent)}
></div>
<div
className="rb:w-8 rb:h-8 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteSubAgent(agent)}
></div>
</Space>
</Flex>
))}
</Card>
{values?.orchestration_mode !== 'collaboration' && <Card title={t('application.masterConfig')}>
<Form.Item
label={t('application.model')}
required={true}
<Card
title={<>
{t('application.subAgentsManagement')}
<span className="rb:font-medium rb:font-[PingFangSC,PingFang_SC]! rb:text-[14px]!"> ({subAgents.length}/{MAX_LENGTH})</span>
</>}
extra={<Button className="rb:py-0! rb:px-2! rb:h-6!" disabled={subAgents.length >= MAX_LENGTH} onClick={() => handleSubAgentModal()}>+ {t('application.addSubAgent')}</Button>}
>
<Row gutter={16}>
<Col span={16}>
{subAgents.length === 0
? <div className="rb-border rb:rounded-xl rb:pt-4 rb:pb-6"><Empty size={88} /></div>
: <Flex vertical gap={12}>
{subAgents.map((agent, index) => (
<Flex key={index} align="center" justify="space-between"
className="rb:w-full! rb-border rb:rounded-xl rb:py-2.5! rb:pl-4! rb:pr-3!"
>
<Flex justify="center" vertical className="rb:max-w-[calc(100%-60px)]">
<div>
<span className="rb:text-[#212332] rb:leading-5">{agent.name}</span>
<Tag color={agent.is_active ? 'success' : 'warning'} className="rb:ml-2">
{agent.is_active ? t('common.enable') : t('common.deleted')}
</Tag>
</div>
{agent.role && <div className="rb:font-regular rb:leading-5 rb:text-[#5B6167] rb:text-[12px] rb:mt-1">{agent.role || '-'}</div>}
{agent.capabilities && <Flex wrap gap={8} className="rb:mt-2.5!">
{agent.capabilities.map((tag, tagIndex) => <Tag key={tagIndex} color="dark" className="rb:py-0!">{tag}</Tag>)}
</Flex>}
</Flex>
<Space>
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleSubAgentModal(agent)}
></div>
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteSubAgent(agent)}
></div>
</Space>
</Flex>
))}
</Flex>
}
</Card>
{values?.orchestration_mode !== 'collaboration' && <Card title={t('application.masterConfig')}>
<Form.Item
label={<span className="rb:text-[#5B6167]">{t('application.model')}</span>}
required={true}
className="rb:mb-4!"
>
<Flex align="center" gap={12}>
<Form.Item name="default_model_config_id" noStyle>
<CustomSelect
url={getModelListUrl}
@@ -270,62 +277,73 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
style={{ width: '100%' }}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="model_parameters" noStyle>
<Button onClick={handleEditModelConfig}>{t('application.modelConfig')}</Button>
<Button
className="rb:w-33"
icon={<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/application/set.svg')]"></div>}
onClick={handleEditModelConfig}
>{t('application.modelConfig')}</Button>
</Form.Item>
</Col>
</Row>
</Form.Item>
<Form.Item
name={['execution_config',"sub_agent_execution_mode"]}
label={t('application.orchestrationMode')}
>
<Select
options={['sequential', 'parallel'].map((type) => ({
value: type,
label: t(`application.${type}`),
}))}
/>
</Form.Item>
<Form.Item
name="aggregation_strategy"
label={t('application.aggregationStrategy')}
>
<Select
options={['merge', 'vote', 'priority'].map((type) => ({
value: type,
label: t(`application.${type}`),
}))}
/>
</Form.Item>
</Card>}
</Space>
</Form>
</Col>
<Col span={12} className="rb:h-full rb:overflow-x-hidden rb:p-[20px_16px_24px_16px]">
<RbCard height="100%" bodyClassName="rb:p-[0]! rb:h-full rb:overflow-hidden">
<Chat
data={data as Config}
chatList={chatList}
updateChatList={setChatList}
handleSave={handleSave}
source="multi_agent"
/>
</RbCard>
</Col>
</Flex>
</Form.Item>
<Form.Item
name={['execution_config',"sub_agent_execution_mode"]}
label={<span className="rb:text-[#5B6167]">{t('application.orchestrationMode')}</span>}
className="rb:mb-4!"
>
<Select
options={['sequential', 'parallel'].map((type) => ({
value: type,
label: t(`application.${type}`),
}))}
/>
</Form.Item>
<Form.Item
name="aggregation_strategy"
label={<span className="rb:text-[#5B6167]">{t('application.aggregationStrategy')}</span>}
className="rb:mb-0!"
>
<Select
options={['merge', 'vote', 'priority'].map((type) => ({
value: type,
label: t(`application.${type}`),
}))}
/>
</Form.Item>
</Card>}
</Flex>
</Form>
</Col>
<Col span={12} className="rb:h-full rb:overflow-y-hidden">
<RbCard
title={t('application.debuggingAndPreview')}
headerType="borderless"
headerClassName="rb:h-[56px]! rb:leading-[22px]!"
titleClassName="rb:font-[MiSans-Bold] rb:font-bold"
bodyClassName="rb:p-4! rb:pt-0!"
className="rb:h-full"
>
<Chat
data={data as Config}
chatList={chatList}
updateChatList={setChatList}
handleSave={handleSave}
source="multi_agent"
/>
</RbCard>
</Col>
<SubAgentModal
ref={subAgentModalRef}
refresh={refreshSubAgents}
/>
<ModelConfigModal
data={values as Config}
ref={modelConfigModalRef}
refresh={handleSaveModelConfig}
/>
</Row>
<SubAgentModal
ref={subAgentModalRef}
refresh={refreshSubAgents}
/>
<ModelConfigModal
data={values as Config}
ref={modelConfigModalRef}
refresh={handleSaveModelConfig}
/>
</Row>
</>
)
})

View File

@@ -1,13 +1,13 @@
/*
* @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-03 19:02:43
*/
import { type FC, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import { Button, Space, Input, Form, App } from 'antd';
import { Space, Input, Form, App, Flex } from 'antd';
import Tag, { type TagProps } from './components/Tag'
import RbCard from '@/components/RbCard/Card'
@@ -19,6 +19,7 @@ import type { Application } from '@/views/ApplicationManagement/types'
import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format';
import Markdown from '@/components/Markdown'
import RbButton from '@/components/RbButton';
/**
* Tag color mapping for release versions
*/
@@ -28,6 +29,7 @@ const tagColors: Record<Release['tagKey'], TagProps['color']> = {
history: 'default',
}
const heightClass = 'rb:h-[calc(100vh-88px)]'
/**
* Release page component
* Manages application version releases, rollbacks, and version history
@@ -68,12 +70,12 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
})
}
return (
<div className="rb:flex rb:h-[calc(100vh-64px)]">
<div className="rb:h-full rb:overflow-y-auto rb:w-108 rb:flex-[0_0_auto] rb:border-r rb:border-[#DFE4ED] rb:p-4">
<Space size={16} direction="vertical" style={{ width: '100%' }}>
<div className="rb:leading-5.5 rb:px-1">
{t('application.versionList')}
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-1 rb:leading-4">{t('application.versionListDesc')}</div>
<Flex gap={12}>
<div className={`rb:overflow-y-auto rb:w-101 rb:flex-[0_0_auto] ${heightClass}`}>
<Flex gap={12} vertical>
<div className="rb:px-1">
<div className="rb:text-[16px] rb:leading-5.5 rb:font-medium">{t('application.versionList')}</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4.5">{t('application.versionListDesc')}</div>
</div>
{releaseList.length === 0
? <Empty />
@@ -91,9 +93,9 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
{tagKey}
</Tag>}
</>}
className={clsx("rb:hover:border-[#155EEF]! rb:cursor-pointer", {
'rb:bg-[rgba(21,94,239,0.06)]! rb:border-[#155EEF]!': version.id === selectedVersion.id,
'rb:border-[#DFE4ED] rb:bg-[#FBFDFF]': version.id !== selectedVersion.id
className={clsx("rb:hover:shadow-[0px_2px_8px_0px_rgba(0,0,0,0.2)]! rb:cursor-pointer rb:bg-white", {
'rb:border-[#171719]!': version.id === selectedVersion.id,
'rb:border-[#DFE4ED] ': version.id !== selectedVersion.id
})}
headerType="borderless"
onClick={() => setSelectedVersion(version)}
@@ -101,38 +103,41 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
<div className="rb:leading-5 rb:line-clamp-2 rb:overflow-hidden rb:text-ellipsis rb:whitespace-nowrap">
<Markdown content={version.release_notes} />
</div>
<div className="rb:mt-4 rb:text-[12px] rb:text-[#5B6167] rb:leading-4">
<div className="rb:mt-4 rb:text-[12px] rb:text-[#5B6167] rb:leading-4.5">
{t('application.publishedOn')} {formatDateTime(version.published_at, 'YYYY-MM-DD HH:mm:ss')}
</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-1 rb:leading-4">
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4.5">
{t('application.publisher')}: {version.publisher_name}
</div>
</RbCard>
)
})
}
</Space>
</Flex>
</div>
<div className="rb:h-full rb:overflow-y-auto rb:flex-[1_1_auto] rb:p-4">
<div className={`rb:overflow-y-auto rb:flex-[1_1_auto] ${heightClass}`}>
<Form layout="vertical">
<div className={clsx("rb:leading-5.5 rb:px-1 rb:flex rb:items-center rb:text-[16px] rb:font-medium rb:mb-5.25", {
<Flex align="center" className={clsx("rb:leading-6.5! rb:text-[18px] rb:font-medium rb:mb-4.75!", {
'rb:justify-between': selectedVersion,
'rb:justify-end': !selectedVersion
})}>
{selectedVersion && t('application.DetailsOfVersion', { version: selectedVersion.version_name && selectedVersion.version_name[0].toLocaleLowerCase() === 'v' ? selectedVersion.version_name : selectedVersion.version_name ? `v${selectedVersion.version_name}` : `v${selectedVersion.version}` || '-' })}
{selectedVersion && t('application.detailsOfVersion', { version: selectedVersion.version_name && selectedVersion.version_name[0].toLocaleLowerCase() === 'v' ? selectedVersion.version_name : selectedVersion.version_name ? `v${selectedVersion.version_name}` : `v${selectedVersion.version}` || '-' })}
<Space size={10}>
{selectedVersion && <>
{/* <Button>{t('application.exportDSLFile')}</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>
{/* <RbButton>{t('application.exportDSLFile')}</RbButton> */}
{data.current_release_id !== selectedVersion.id && <RbButton onClick={handleRollback}>{t('application.willRollToThisVersion')}</RbButton>}
<RbButton onClick={() => releaseShareModalRef.current?.handleOpen()}>{t('application.share')}</RbButton>
</>}
<Button type="primary" onClick={() => releaseModalRef.current?.handleOpen()}>{t('application.release')}</Button>
<RbButton type="primary" onClick={() => releaseModalRef.current?.handleOpen()}>{t('application.release')}</RbButton>
</Space>
</div>
</Flex>
{selectedVersion &&
<Space size={16} direction="vertical" style={{ width: '100%' }}>
<RbCard title={t('application.VersionInformation')} headerType="borderless">
<Flex gap={16} vertical>
<RbCard
title={t('application.VersionInformation')}
headerType="borderless"
>
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
<Form.Item label={t('application.releaseTime')} className="rb:mb-0!">
<Input value={formatDateTime(selectedVersion.published_at, 'YYYY-MM-DD HH:mm:ss')} disabled />
@@ -147,22 +152,26 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
</RbCard>
{/* Logs */}
<RbCard title={t('application.changeLog')} headerType="borderless">
<Space size={16} direction="vertical" style={{ width: '100%' }}>
<RbCard
title={t('application.changeLog')}
headerType="borderless"
>
<Flex gap={16} vertical>
{selectedVersion && (
<RbCard
headerType="borderBL"
headerType="borderless"
title={<div className="rb:text-[14px]">{formatDateTime(selectedVersion.published_at, 'YYYY-MM-DD HH:mm:ss')}</div>}
extra={<span className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4">{selectedVersion.publisher_name}</span>}
bodyClassName="rb:pt-0! rb:pb-3! rb:px-4!"
>
<div className="rb:font-medium rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-4">
<div className="rb:font-regular rb:text-[#5B6167] rb:leading-4">
<Markdown content={selectedVersion.release_notes} />
</div>
</RbCard>
)}
</Space>
</Flex>
</RbCard>
</Space>
</Flex>
}
</Form>
</div>
@@ -175,7 +184,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
ref={releaseShareModalRef}
version={selectedVersion}
/>
</div>
</Flex>
);
}
export default ReleasePage;

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:29:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 18:57:44
*/
import { type FC, useState, useEffect } from 'react';
import { Row, Col, Flex, DatePicker } from 'antd';
@@ -13,7 +13,7 @@ const { RangePicker } = DatePicker;
import type { Application } from '@/views/ApplicationManagement/types'
import { getAppStatistics } from '@/api/application';
import LineCard from './components/LineCard'
import ChartCard from './components/ChartCard'
import type { StatisticsData, StatisticsItem } from './types'
/**
@@ -79,11 +79,11 @@ const Statistics: FC<{ application: Application | null }> = ({ application }) =>
})
}
return (
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
<div className="rb:w-250 rb:mx-auto">
<Row gutter={[16, 16]}>
<Col span={24}>
<Flex justify="end">
<RangePicker defaultValue={[query.start_date, query.end_date]} onChange={handleChange} />
<RangePicker className="rb:w-70" defaultValue={[query.start_date, query.end_date]} onChange={handleChange} />
</Flex>
</Col>
{Object.entries(data).map(([key, value]) => {
@@ -93,7 +93,7 @@ const Statistics: FC<{ application: Application | null }> = ({ application }) =>
const totalKey = TotalObj[key];
return (
<Col span={12} key={key}>
<LineCard
<ChartCard
type={key}
total={totalKey ? (data[totalKey] as number) : 0}
chartData={value as StatisticsItem[]}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:44
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-05 10:31:12
* @Last Modified time: 2026-03-04 14:40:55
*/
/**
* AI Prompt Assistant Modal
@@ -11,7 +11,7 @@
*/
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
import { Button, Form, Input, App, Row, Col } from 'antd';
import { Button, Form, Input, App, Flex, Space } from 'antd';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx'
import copy from 'copy-to-clipboard';
@@ -23,13 +23,14 @@ import RbModal from '@/components/RbModal'
import type { ModelListItem } from '@/views/ModelManagement/types'
import ChatContent from '@/components/Chat/ChatContent'
import Empty from '@/components/Empty'
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
import type { ChatItem } from '@/components/Chat/types'
import CustomSelect from '@/components/CustomSelect'
import AiPromptVariableModal from './AiPromptVariableModal'
import { type SSEMessage } from '@/utils/stream'
import Editor from './Editor'
import { getLogoUrl } from '@/views/ModelManagement/utils'
import analysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.png'
/**
* Component props
@@ -39,7 +40,7 @@ interface AiPromptModalProps {
refresh: (value: string) => void;
/** Default model to pre-select */
defaultModel?: ModelListItem | null;
source?: 'app' | 'skills'
source?: 'application' | 'skills'
}
/**
@@ -185,6 +186,13 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
useImperativeHandle(ref, () => ({
handleOpen,
}));
const [isFocus, setIsFocus] = useState(false)
const handleFocus = () => {
setIsFocus(true)
}
const handleBlur = () => {
setIsFocus(false)
}
console.log(values)
return (
@@ -194,69 +202,107 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
onCancel={handleClose}
footer={null}
width={1000}
classNames={{
content: 'rb:p-0!',
header: 'rb:p-6! rb:mb-0!',
body: 'rb:p-0! rb:border-t rb:border-t-[#EBEBEB]'
}}
>
<Form form={form}>
<div className="rb:grid rb:grid-cols-2 rb:border-t rb:border-t-[#EBEBEB]">
<div className="rb:border-r rb:border-r-[#EBEBEB] rb:pr-6 rb:pt-3">
<Form form={form} className="rb:mx-4!">
<div className="rb:grid rb:grid-cols-2">
<div className="rb:border-r rb:border-r-[#EBEBEB] rb:pr-4 rb:pt-3 rb:pb-4">
<Form.Item
label={t(`${source}.model`)}
name="model_id"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{ width: '100%' }}
optionLabelProp="children"
format={(data) => {
return data.map(option => ({
...data,
value: option.id,
label: (<div key={option.id} className="rb:flex rb:items-center rb:gap-2">
{getLogoUrl(option.logo as string) && <img src={getLogoUrl(option.logo as string)} className="rb:inline-block rb:size-4 rb:align-middle" alt="" />}
<span>{option.name as string}</span>
</div>
)
}))
}}
className="rb:w-full"
/>
</Form.Item>
<ChatContent
classNames="rb:h-100.5 rb:px-[16px] rb:py-[20px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]"
contentClassNames="rb:max-w-[260px]!"
empty={<Empty url={ConversationEmptyIcon} title={t(`${source}.promptChatEmpty`)} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
classNames="rb:h-105.5 rb:pb-[15px]!"
contentClassNames="rb:max-w-75!"
empty={<Empty url={ConversationEmptyIcon} title={t(`${source}.promptChatEmpty`)} isNeedSubTitle={false} size={[140, 100]} className="rb:h-full" />}
data={chatList || []}
streamLoading={false}
labelPosition="top"
labelFormat={(item) => item.role === 'user' ? t(`${source}.you`) : t(`${source}.ai`)}
/>
<div className="rb:flex rb:items-center rb:gap-2.5 rb:py-4">
<Form.Item name="message" className="rb:mb-0!" style={{ width: 'calc(100% - 54px)' }}>
<Flex align="center" gap={12} justify="space-between"
className={clsx("rb-border rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.1)] rb:rounded-2xl rb:h-13 rb:px-3!", {
'rb:border rb:border-[#171719]!': isFocus
})}
>
<Form.Item name="message" className="rb:flex-1 rb:mb-0!">
<Input
className="rb:h-11 rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
placeholder={t(`${source}.promptChatPlaceholder`)}
onPressEnter={handleSend}
variant="borderless"
className="rb:p-0!"
onFocus={handleFocus}
onBlur={handleBlur}
/>
</Form.Item>
<img src={ChatSendIcon} className={clsx("rb:w-11 rb:h-11 rb:cursor-pointer", {
'rb:opacity-50': loading,
})} onClick={handleSend} />
</div>
{loading
? <div className="rb:size-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/loading.svg')]"></div>
: !values || !values?.message || values?.message?.trim() === ''
? <div className="rb:size-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/sendDisabled.svg')]"></div>
: <div className="rb:size-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/send.svg')]" onClick={handleSend}></div>
}
</Flex>
</div>
<div className="rb:pl-6 rb:pt-3">
<Row>
<Col span={source === 'application' ? 12 : 24}>
<Form.Item label={t(`${source}.conversationOptimizationPrompt`)}></Form.Item>
</Col>
{source === 'application' && <Col span={12} className="rb:text-right">
<Button onClick={handleAdd}>+ {t(`${source}.addVariable`)}</Button>
</Col>}
</Row>
<Form.Item name="current_prompt">
<Editor
ref={editorRef}
className="rb:h-100.5 "
onChange={(value) => form.setFieldValue('current_prompt', value)}
/>
<div className="rb:pl-4 rb:pt-3.5 rb:pb-4">
<Flex justify="space-between" className="rb:mb-3!">
<div>
{t(`${source}.conversationOptimizationPrompt`)}
</div>
<Space size={8}>
<Button
disabled={!values?.current_prompt}
icon={<div className="rb:size-3.5 rb:bg-cover rb:bg-[url('@/assets/images/application/copy.svg')]"></div>}
onClick={handleCopy}>{t('common.copy')}</Button>
<Button
disabled={!values?.current_prompt}
icon={<div className="rb:size-3.5 rb:bg-cover rb:bg-[url('@/assets/images/application/save.svg')]"></div>}
onClick={handleApply}
>{t(`${source}.apply`)}</Button>
{source === 'application' &&
<Button
disabled={!values?.current_prompt}
icon={<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/plus_dark.svg')]"></div>}
onClick={handleAdd}
></Button>
}
</Space>
</Flex>
<Form.Item name="current_prompt" noStyle>
{values?.current_prompt
? <Editor
ref={editorRef}
className="rb:h-119 rb:bg-white! rb:border-none! rb:p-0!"
onChange={(value) => form.setFieldValue('current_prompt', value)}
/>
: <Empty url={analysisEmptyIcon} title={t(`${source}.promptOptimizationEmpty`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-119 rb:w-70 rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
}
</Form.Item>
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-6">
<Button block disabled={!values?.current_prompt} onClick={handleCopy}>{t('common.copy')}</Button>
<Button type="primary" block disabled={!values?.current_prompt} onClick={handleApply}>{t(`${source}.apply`)}</Button>
</div>
</div>
</div>
</Form>

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:22
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:27:22
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-11 11:52:32
*/
/**
* API Key Configuration Modal
@@ -10,7 +10,7 @@
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Slider } from 'antd';
import { Form, Slider, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ApiKeyConfigModalRef } from '../types'
@@ -111,10 +111,10 @@ interface ApiKeyConfigModalProps {
step={1}
/>
</Form.Item>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
<Flex align="center" justify="space-between" className="rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5!">
1
<span>{t('application.currentValue')}: {values?.rate_limit}{t('application.qpsLimitUnit')}</span>
</div>
<span>{t('application.currentValue')}: {values?.rate_limit} {t('application.qpsLimitUnit')}</span>
</Flex>
</div>
</>
{/* Daily usage limit */}
@@ -136,10 +136,10 @@ interface ApiKeyConfigModalProps {
step={100}
/>
</Form.Item>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
<Flex align="center" justify="space-between" className="rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5!">
100
<span>{t('application.currentValue')}: {values?.daily_request_limit}{t('application.dailyUsageLimitUnit')}</span>
</div>
<span>{t('application.currentValue')}: {values?.daily_request_limit} {t('application.dailyUsageLimitUnit')}</span>
</Flex>
</div>
</>
</Form>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:31
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 13:50:47
* @Last Modified time: 2026-03-04 10:25:35
*/
import { type FC, type ReactNode } from 'react'
@@ -40,8 +40,9 @@ const Card: FC<CardProps> = ({
subTitle={subTitle}
extra={extra}
variant={variant}
headerType="borderL"
headerClassName={variant ? '' : "rb:before:bg-[#155EEF]! rb:before:h-[19px]"}
headerType="borderless"
headerClassName="rb:h-11.5! rb:py-3! rb:leading-5.5!"
titleClassName="rb:font-[MiSans-Bold] rb:font-bold"
>
{children}
</RbCard>

View File

@@ -0,0 +1,98 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:28:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-25 13:55:11
*/
/**
* Line chart card component for displaying statistics
* Uses ECharts to render time-series data with gradient area fill
*/
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import type { StatisticsItem } from '../types'
import RbCard from '@/components/RbCard/Card';
import AreaLineChart from '@/components/Charts/AreaLineChart'
import BarChart from '@/components/Charts/BarChart'
/**
* Component props
*/
interface ChartCardProps {
/** Chart data points */
chartData: StatisticsItem[];
/** Statistics type key */
type: string;
/** Total count to display */
total: number;
}
/**
* Color mapping for different statistic types
*/
const ColorObj: Record<string, string> = {
daily_conversations: '#155EEF',
daily_new_users: '#9C6FFF',
daily_api_calls: '#155EEF',
daily_tokens: '#FF8A4C'
}
/**
* Line chart card component
* Displays time-series statistics with gradient area chart
*/
const ChartCard: FC<ChartCardProps> = ({ chartData, type, total }) => {
const { t } = useTranslation()
return (
<RbCard
title={t(`application.${type}`)}
subTitle={<span className="rb:font-[MiSans-Bold] rb:text-[#171719] rb:font-bold rb:text-[28px] rb:leading-9.5">{total}</span>}
headerType="borderless"
headerClassName="rb:min-h-26!"
>
<div className="rb:h-50">
{type === 'daily_conversations' || type === 'daily_tokens' ? (
<AreaLineChart
chartData={chartData}
colors={[ColorObj[type]]}
xAxisKey="date"
seriesList={{ count: t(`application.${type}`) }}
height={200}
lineStyle={{width: 3}}
showLegend={false}
grid={{
top: 7,
left: 4,
right: 18,
bottom: 0,
containLabel: true
}}
smooth={type === 'daily_conversations'}
/>
)
: <BarChart
chartData={chartData}
colors={[ColorObj[type]]}
xAxisKey="date"
seriesList={{ count: t(`application.${type}`) }}
height={200}
showLegend={false}
grid={{
top: 7,
left: 4,
right: 18,
bottom: 0,
containLabel: true
}}
itemStyle={type === 'daily_new_users' ? { color: ColorObj[type] } : null}
showBackground={type === 'daily_api_calls'}
/>}
</div>
</RbCard>
)
}
export default ChartCard

View File

@@ -10,7 +10,7 @@
* Provides real-time streaming responses and conversation history
*/
import { type FC, useEffect, useState, useRef } from 'react';
import { type FC, useEffect, useState, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx'
import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd'
@@ -143,18 +143,16 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
const modelChatList = [...prev]
const curModelChat = modelChatList[targetIndex]
const curChatMsgList = curModelChat.list || []
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
if (lastMsg.role === 'assistant') {
modelChatList[targetIndex] = {
...modelChatList[targetIndex],
list: [
...curChatMsgList.slice(0, curChatMsgList.length - 1),
{
...lastMsg,
content: null
}
]
}
const lastMsg = curChatMsgList[curChatMsgList.length - 2]
modelChatList[targetIndex] = {
...modelChatList[targetIndex],
list: [
...curChatMsgList.slice(0, curChatMsgList.length - 2),
{
...lastMsg,
...(lastMsg.role === 'user' ? { status: 'error' } : { content: null })
}
]
}
return [...modelChatList]
}
@@ -434,62 +432,71 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
const updateFileList = (list?: any[]) => {
setFileList([...list || []])
}
const isHasLabel = useMemo(() => chatList.some(item => item.label), [chatList])
return (
<div className="rb:relative rb:h-full rb:flex rb:flex-col">
<Flex vertical className="rb:relative rb:h-full">
{chatList.length === 0
? <Empty
url={DebuggingEmpty}
size={[300, 200]}
title={t('application.debuggingEmpty')}
subTitle={t('application.debuggingEmptyDesc')}
className="rb:h-full"
title={t('application.debuggingEmpty')}
subTitle={t('application.debuggingEmptyDesc')}
className="rb:h-[calc(100vh-159px)]"
/>
: <>
<div className={clsx(`rb:relative rb:grid rb:grid-cols-${chatList.length} rb:overflow-hidden rb:w-full rb:flex-1 rb:min-h-0`)}>
{chatList.map((chat, index) => (
<div key={index} className={clsx('rb:flex rb:flex-col', {
"rb:border-r rb:border-[#DFE4ED]": index !== chatList.length - 1 && chatList.length > 1,
})}>
{chat.label &&
<div className={clsx(
"rb:grid rb:bg-[#F0F3F8] rb:text-center rb:flex-[0_0_auto]",
{
'rb:rounded-tr-xl': index === chatList.length - 1,
'rb:rounded-tl-xl': index === 0,
}
)}>
<div className='rb:relative rb:p-[10px_12px] rb:overflow-hidden'>
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:w-[calc(100%-24px)]">{chat.label}</div>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:absolute rb:top-3 rb:right-3 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
onClick={() => handleDelete(index)}
></div>
</div>
: <>
<div className={clsx(`rb:relative rb:grid rb:grid-cols-${chatList.length} rb:overflow-hidden rb:w-full rb:flex-1 rb:min-h-0`)}>
{chatList.map((chat, index) => (
<Flex key={index} vertical className={clsx({
"rb:border-r rb:border-[#DFE4ED]": index !== chatList.length - 1 && chatList.length > 1,
})}>
{chat.label &&
<div className={clsx(
"rb:grid rb:bg-[#F6F6F6] rb:text-center rb:flex-[0_0_auto]"
)}>
<div className='rb:relative rb:py-2.5 rb:px-3 rb:overflow-hidden'>
<div className="rb:text-[#212332] rb:font-medium rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:w-[calc(100%-24px)]">{chat.label}</div>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:absolute rb:top-3 rb:right-3 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
onClick={() => handleDelete(index)}
></div>
</div>
}
<ChatContent
classNames={{
'rb:mx-[16px] rb:mt-6': true,
'rb:h-[calc(100vh-282px)]': isCluster,
'rb:h-[calc(100vh-380px)]': !isCluster,
}}
contentClassNames={{
'rb:max-w-[400px]!': chatList.length === 1,
'rb:max-w-[260px]!': chatList.length === 2,
'rb:max-w-[150px]!': chatList.length === 3,
'rb:max-w-[108px]!': chatList.length === 4,
}}
empty={<Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
data={chat.list || []}
streamLoading={compareLoading}
labelPosition="top"
labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label}
errorDesc={t('application.ReplyException')}
/>
</div>
))}
</div>
</div>
}
<ChatContent
classNames={{
'rb:mb-3 rb:mt-5': isHasLabel,
'rb:mb-3': !isHasLabel,
'rb:h-[calc(100vh-292px)]': isCluster,
'rb:h-[calc(100vh-353px)]': !isCluster,
"rb:pr-4": index !== chatList.length - 1 && chatList.length > 1,
"rb:pl-4": index !== 0 && chatList.length > 1,
}}
contentClassNames={{
'rb:max-w-100!': chatList.length === 1,
'rb:max-w-70!': chatList.length === 2,
'rb:max-w-45!': chatList.length === 3,
'rb:max-w-24!': chatList.length === 4,
}}
empty={<Empty
url={ChatIcon}
title={t('application.chatEmpty')}
isNeedSubTitle={false}
size={[240, 200]}
className={clsx({
"rb:h-[calc(100vh-353px)]": isHasLabel,
"rb:h-[calc(100vh-292px)]": !isHasLabel,
})}
/>}
data={chat.list || []}
streamLoading={compareLoading}
labelPosition="top"
labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label || t(`application.ai`)}
errorDesc={t('application.ReplyException')}
/>
</Flex>
))}
</div>
<div className="rb:relative rb:flex rb:items-center rb:gap-2.5 rb:m-4 rb:mb-1">
<ChatInput
message={message}
@@ -527,16 +534,16 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
<Divider type="vertical" className="rb:ml-1.5! rb:mr-3!" />
</Flex>
</Flex>
</ChatInput>
</ChatInput>
</div>
</>
</>
}
<UploadFileListModal
ref={uploadFileListModalRef}
refresh={addFileList}
/>
</div>
</Flex>
)
}

View File

@@ -2,16 +2,16 @@
* @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-04 10:31:08
*/
import { type FC, useRef, useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Layout, Tabs, Dropdown, Button, Flex } from 'antd';
import { Tabs, Dropdown, Button, Flex } from 'antd';
import type { MenuProps } from 'antd';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import styles from '../index.module.css'
import logoutIcon from '@/assets/images/logout.svg'
import editIcon from '@/assets/images/edit_hover.svg'
import copyIcon from '@/assets/images/copy_hover.svg'
import exportIcon from '@/assets/images/export_hover.svg'
@@ -21,10 +21,9 @@ import ApplicationModal from '@/views/ApplicationManagement/components/Applicati
import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef } from '../types'
import { deleteApplication } from '@/api/application'
import CopyModal from './CopyModal'
import PageHeader from '@/components/Layout/PageHeader'
import { exportToYaml } from '@/utils/yamlExport';
const { Header } = Layout;
/**
* Tab keys for application configuration
*/
@@ -93,8 +92,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
copyModalRef.current?.handleOpen()
break;
case 'export':
console.log('export', workflowRef?.current?.config)
exportToYaml(workflowRef?.current?.config, application?.name ?`${application?.name}.yml`: undefined)
exportToYaml(workflowRef?.current?.config, application?.name ? `${application?.name}.yml` : undefined)
break;
case 'delete':
handleDelete()
@@ -153,7 +151,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
* Format dropdown menu items
*/
const formatMenuItems = useMemo(() => {
const items = (application?.type === 'workflow' ? ['edit', 'copy', 'export', 'delete'] : ['edit', 'copy', 'delete']).map(key => ({
const items = (application?.type === 'workflow' ? ['edit', 'copy', 'export', 'delete'] : ['edit', 'copy', 'delete']).map(key => ({
key,
icon: <img src={menuIcons[key]} className="rb:w-4 rb:h-4 rb:mr-2" />,
label: t(`common.${key}`),
@@ -164,49 +162,53 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
console.log('formatMenuItems', formatMenuItems)
return (
<>
<Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
<div className="rb:h-8 rb:flex rb:items-center rb:font-medium">
<div className="rb:w-8 rb:h-8 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[24px] rb:text-[#ffffff]">
{application?.name[0]}
</div>
<div className="rb:max-w-[100%-80px] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{application?.name}</div>
<Dropdown
menu={{ items: formatMenuItems, onClick: handleClick }}
trigger={['click']}
placement="bottomRight"
>
<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>
</div>
<div className="rb:flex rb:justify-center">
<Tabs
activeKey={activeTab}
items={formatTabItems()}
onChange={handleChangeTab}
<PageHeader
avatarText={application?.name?.trim()[0]}
avatarClassName={clsx({
'rb:bg-[#155EEF]': application?.type === 'agent',
'rb:bg-[#9C6FFF]!': application?.type === 'multi_agent',
'rb:bg-[#171719]': application?.type === 'workflow',
})}
title={application?.name || ''}
operation={<Dropdown
menu={{ items: formatMenuItems, onClick: handleClick }}
trigger={['click']}
placement="bottomRight"
>
<div
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit_active.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
></div>
</Dropdown>}
centerContent={<Flex justify="center" className="rb:h-16!">
<Tabs
activeKey={activeTab}
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">
</Flex>}
extra={application?.type === 'workflow'
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
<Button onClick={clear}>{t('workflow.clear')}</Button>
<Button onClick={addvariable}>{t('workflow.addvariable')}</Button>
<Button onClick={run}>{t('workflow.run')}</Button>
<Button type="primary" onClick={save}>{t('workflow.save')}</Button>
{/* <Button type="primary">{t('workflow.export')}</Button> */}
<img src={logoutIcon} className="rb:w-4 rb:h-4 rb:cursor-pointer" onClick={goToApplication} />
</div>
<div
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/logout.svg')]"
onClick={goToApplication}
></div>
</Flex>
: <Flex justify="flex-end">
<div className="rb:h-8 rb:flex rb:items-center rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
{t('application.returnToApplicationList')}
</div>
<Flex align="center" className="rb:leading-5 rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
<div
className="rb:mr-2 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/logout.svg')]"
></div>
{t('common.return')}
</Flex>
</Flex>
}
</Header>
>
</PageHeader>
<ApplicationModal
ref={applicationModalRef}
refresh={refresh}

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-26 11:18:04
*/
/**
* Rich text editor component using Lexical framework
@@ -136,14 +136,14 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
contentEditable={
<ContentEditable
className={clsx(
"rb:outline-none rb:resize-none rb:text-[14px] rb:leading-5 rb:px-4 rb:py-5 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:overflow-auto",
"rb:outline-none rb:resize-none rb:text-[14px] rb:leading-5 rb:px-4 rb:py-5 rb:bg-[#FBFDFF] rb-border rb:rounded-lg rb:overflow-auto",
disabled && "rb:cursor-not-allowed rb:bg-[#F6F8FC] rb:text-[#5B6167]",
className
)}
/>
}
placeholder={
<div className="rb:absolute rb:px-4 rb:py-5 rb:text-[14px] rb:text-[#5B6167] rb:leading-5 rb:pointer-none">
<div className="rb:absolute rb:top-0 rb:px-4 rb:py-5 rb:text-[14px] rb:text-[#5B6167] rb:leading-5 rb:pointer-none">
{placeholder}
</div>
}

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:32
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:32
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 10:34:43
*/
/**
* Knowledge Base Component
@@ -12,7 +12,7 @@
import { type FC, useRef, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Space, Button, List } from 'antd'
import { Space, Button, Flex } from 'antd'
import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg'
import type {
@@ -140,44 +140,48 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
title={t('application.knowledgeBaseAssociation')}
extra={
<Space>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleKnowledgeConfig}>{t('application.globalConfig')}</Button>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddKnowledge}>+</Button>
<Button className="rb:h-6! rb:py-0! rb:px-2! rb:rounded-md! rb:text-[#21233"
icon={<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/application/set.svg')]"></div>}
onClick={handleKnowledgeConfig}
>{t('application.globalConfig')}</Button>
<Button className="rb:h-6! rb:py-0! rb:px-2! rb:rounded-md! rb:text-[#21233" onClick={handleAddKnowledge}>+</Button>
</Space>
}
>
<div className="rb:leading-4.5 rb:text-[12px] rb:mb-2 rb:font-medium">
{t('application.associatedKnowledgeBase')}
</div>
{knowledgeList.length === 0
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
:
<List
grid={{ gutter: 12, column: 1 }}
dataSource={knowledgeList}
renderItem={(item) => {
? <div className="rb-border rb:rounded-xl rb:min-h-37">
<Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} className="rb:mt-4!" />
</div>
: <Flex vertical gap={10}>
{knowledgeList.map(item => {
if (!item.id) return null
return (
<List.Item>
<div key={item.id} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<div className="rb:font-medium rb:leading-4">
{item.name}
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-2">
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
</Tag>
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', {include_count: item.doc_num})}</div>
</div>
<Space size={12}>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleEditKnowledge(item)}
></div>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteKnowledge(item.id)}
></div>
</Space>
<Flex key={item.id} align="center" justify="space-between" className="rb:py-3! rb:px-4! rb-border rb:rounded-lg">
<div>
<span className="rb:font-medium rb:leading-4">{item.name}</span>
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-2">
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
</Tag>
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t('application.contains', {include_count: item.doc_num})}</div>
</div>
</List.Item>
<Space size={12}>
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleEditKnowledge(item)}
></div>
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteKnowledge(item.id)}
></div>
</Space>
</Flex>
)
}}
/>
})}
</Flex>
}
<KnowledgeGlobalConfigModal
data={editConfig}

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:37
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:37
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-24 11:47:27
*/
/**
* Knowledge Configuration Modal
@@ -11,7 +11,7 @@
*/
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { Form, Select, InputNumber } from 'antd';
import { Form, Select, InputNumber, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import type { KnowledgeConfigModalRef, KnowledgeBase, KnowledgeConfigForm, RetrieveType } from './types'
@@ -109,13 +109,13 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
layout="vertical"
>
{data && (
<div className="rb:mb-6 rb:flex rb:items-center rb:justify-between rb:border rb:rounded-lg rb:p-[17px_16px] rb:cursor-pointer rb:bg-[#F0F3F8] rb:border-[#DFE4ED] rb:text-[#212332]">
<Flex align="center" justify="space-between" className="rb:mb-6! rb-border rb:rounded-lg rb:py-4.25! rb:px-4! rb:cursor-pointer rb:bg-[#F0F3F8] rb:text-[#212332]">
<div className="rb:text-[16px] rb:leading-5.5">
{data.name}
<div className="rb:text-[12px] rb:leading-4 rb:text-[#5B6167] rb:mt-2">{t('application.contains', {include_count: data.doc_num})}</div>
</div>
<div className="rb:text-[12px] rb:leading-4 rb:text-[#5B6167]">{formatDateTime(data.updated_at, 'YYYY-MM-DD HH:mm:ss')}</div>
</div>
</Flex>
)}
<FormItem name="kb_id" hidden />
{/* Retrieval mode */}

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:42
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:42
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-24 11:47:32
*/
/**
* Knowledge Global Configuration Modal
@@ -10,7 +10,7 @@
*/
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import { Form, InputNumber, Switch } from 'antd';
import { Form, InputNumber, Switch, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import type { RerankerConfig, KnowledgeGlobalConfigModalRef } from './types'
@@ -94,7 +94,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
<div className="rb:text-[#5B6167] rb:mb-6">{t('application.globalConfigDesc')}</div>
{/* Result reranking */}
<div className="rb:flex rb:items-center rb:justify-between rb:my-6">
<Flex align="center" justify="space-between" className="rb:my-6!">
<div className="rb:text-[14px] rb:font-medium rb:leading-5">
{t('application.rerankModel')}
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t('application.rerankModelDesc')}</div>
@@ -106,7 +106,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
>
<Switch />
</FormItem>
</div>
</Flex>
{values?.rerank_model && <>
<FormItem

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:25:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:25:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 10:39:34
*/
/**
* Knowledge List Modal
@@ -10,7 +10,7 @@
*/
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { Space, List } from 'antd';
import { List, Form, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx'
@@ -42,14 +42,16 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
const [visible, setVisible] = useState(false);
const [list, setList] = useState<KnowledgeBaseListItem[]>([])
const [filterList, setFilterList] = useState<KnowledgeBaseListItem[]>([])
const [query, setQuery] = useState<{keywords?: string}>({})
const [selectedIds, setSelectedIds] = useState<string[]>([])
const [selectedRows, setSelectedRows] = useState<KnowledgeBase[]>([])
const [form] = Form.useForm()
const query = Form.useWatch([], form)
/** Close modal and reset state */
const handleClose = () => {
setVisible(false);
setQuery({})
form.resetFields()
setSelectedIds([])
setSelectedRows([])
};
@@ -57,7 +59,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
/** Open modal */
const handleOpen = () => {
setVisible(true);
setQuery({})
form.resetFields()
setSelectedIds([])
setSelectedRows([])
};
@@ -66,7 +68,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
if (visible) {
getList()
}
}, [query.keywords, visible])
}, [query?.keywords, visible])
/** Fetch knowledge base list */
const getList = () => {
getKnowledgeBaseList(undefined, {
@@ -77,7 +79,9 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
})
.then(res => {
const response = res as { items: KnowledgeBaseListItem[] }
setList(response.items || [])
setList([...(response.items || [])])
setSelectedIds([])
setSelectedRows([])
})
}
/** Save selected knowledge bases */
@@ -99,12 +103,6 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
handleOpen,
handleClose
}));
/** Search knowledge bases */
const handleSearch = (value?: string) => {
setQuery({keywords: value})
setSelectedIds([])
setSelectedRows([])
}
/** Toggle knowledge base selection */
const handleSelect = (item: KnowledgeBase) => {
const index = selectedIds.indexOf(item.id)
@@ -121,7 +119,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
if (list.length && selectedList.length) {
const unSelectedList = list.filter(item => selectedList.findIndex(vo => vo.id === item.id) < 0)
setFilterList([...unSelectedList])
} else if (list.length) {
} else {
setFilterList([...list])
}
}, [list, selectedList])
@@ -136,12 +134,15 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
onOk={handleSave}
width={1000}
>
<Space size={24} direction="vertical" className="rb:w-full">
<SearchInput
placeholder={t('knowledgeBase.searchPlaceholder')}
onSearch={handleSearch}
style={{ width: '100%' }}
/>
<Flex gap={24} vertical>
<Form form={form}>
<Form.Item name="keywords" noStyle>
<SearchInput
placeholder={t('knowledgeBase.searchPlaceholder')}
className="rb:w-full!"
/>
</Form.Item>
</Form>
{filterList.length === 0
? <Empty />
: <List
@@ -149,7 +150,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
dataSource={filterList}
renderItem={(item: KnowledgeBase) => (
<List.Item>
<div key={item.id} className={clsx("rb:flex rb:items-center rb:justify-between rb:border rb:rounded-lg rb:p-[17px_16px] rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
<Flex key={item.id} align="center" justify="space-between" className={clsx("rb:border rb:rounded-lg rb:py-4.25! rb:px-4! rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": selectedIds.includes(item.id),
"rb:border-[#DFE4ED] rb:text-[#212332]": !selectedIds.includes(item.id),
})} onClick={() => handleSelect(item)}>
@@ -158,12 +159,12 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
<div className="rb:text-[12px] rb:leading-4 rb:text-[#5B6167] rb:mt-2">{t('application.contains', {include_count: item.doc_num})}</div>
</div>
<div className="rb:text-[12px] rb:leading-4 rb:text-[#5B6167]">{formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')}</div>
</div>
</Flex>
</List.Item>
)}
/>
}
</Space>
</Flex>
</RbModal>
</>
);

View File

@@ -1,154 +0,0 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:28:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:28:03
*/
/**
* Line chart card component for displaying statistics
* Uses ECharts to render time-series data with gradient area fill
*/
import { type FC, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react';
import * as echarts from 'echarts';
import Empty from '@/components/Empty'
import Card from './Card'
import type { StatisticsItem } from '../types'
/**
* Component props
*/
interface LineCardProps {
/** Chart data points */
chartData: StatisticsItem[];
/** Statistics type key */
type: string;
/** Total count to display */
total: number;
}
/**
* ECharts series configuration
*/
const SeriesConfig = {
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 3
},
showSymbol: true,
label: {
show: false,
position: 'top'
},
emphasis: {
focus: 'series'
},
}
/**
* Color mapping for different statistic types
*/
const ColorObj: Record<string, string> = {
daily_conversations: '#FFB048',
daily_new_users: '#4DA8FF',
daily_api_calls: '#155EEF',
daily_tokens: '#AD88FF'
}
/**
* Line chart card component
* Displays time-series statistics with gradient area chart
*/
const LineCard: FC<LineCardProps> = ({ chartData, type, total }) => {
const { t } = useTranslation()
const chartRef = useRef<ReactEcharts>(null);
useEffect(() => {
}, [chartData])
const getSeries = () => {
return [{
...SeriesConfig,
name: t(`application.${type}`),
data: chartData.map(vo => vo.count),
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: ColorObj[type] },
{ offset: 1, color: '#FFFFFF' }
])
},
}]
}
return (
<Card
title={<div>{t(`application.${type}`)} <span className="rb:text-[#155EEF] rb:font-medium rb:text-[18px]">{total}</span></div>}
>
{chartData && chartData.length > 0 ? (
<ReactEcharts
ref={chartRef}
option={{
color: [ColorObj[type]],
tooltip: {
trigger: 'axis',
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
axisPointer: {
type: 'line',
crossStyle: {
color: '#5F6266',
},
lineStyle: {
color: '#5F6266',
},
label: {
show: false
}
},
},
grid: {
top: 10,
left: 15,
right: 40,
bottom: 0,
containLabel: true
},
xAxis: {
type: 'category',
data: chartData.map(item => item.date),
boundaryGap: false,
},
yAxis: {
type: 'value',
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC',
align: 'right',
lineHeight: 17,
},
axisLine: {
lineStyle: {
color: '#EBEBEB',
}
},
},
series: getSeries()
}}
style={{ height: '265px', width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
opts={{ renderer: 'canvas' }}
notMerge={true}
lazyUpdate={true}
/>
) : <Empty size={120} className="rb:mt-12 rb:mb-20.25" />}
</Card>
)
}
export default LineCard

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:28:46
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 14:03:44
* @Last Modified time: 2026-03-04 10:32:29
*/
/**
* Release Share Modal
@@ -10,7 +10,7 @@
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Button, App } from 'antd';
import { Button, App, Flex } from 'antd';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import copy from 'copy-to-clipboard'
@@ -86,13 +86,15 @@ const ReleaseShareModal = forwardRef<ReleaseShareModalRef, ReleaseShareModalProp
>
<>
<div className="rb:leading-5 rb:mb-2">{t('application.shareLink')}</div>
<div className="rb:mb-3 rb:flex rb:items-center rb:gap-2.5 rb:justify-between">
<div className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis rb:cursor-pointer rb:h-8 rb:p-[6px_10px] rb:bg-[#FFFFFF] rb:border rb:border-[#EBEBEB] rb:rounded-md rb:leading-5">{shareLink}</div>
<Flex align="center" justify="space-between" gap={10} className="rb:mb-3!">
<div className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis rb:cursor-pointer rb:h-8 rb:p-[6px_10px] rb:bg-[#FFFFFF] rb:border rb:border-[#EBEBEB] rb:rounded-md rb:leading-5" onClick={handleCopy}>
{shareLink}
</div>
<Button type="primary" loading={loading} disabled={!shareLink} onClick={handleCopy}>
{t('common.copy')}
</Button>
</div>
</Flex>
<RbAlert color="orange" icon={<ExclamationCircleFilled />}>
{t('application.shareLinkTip')}
</RbAlert>

View File

@@ -2,10 +2,10 @@
* @Author: ZhaoYing
* @Date: 2026-02-05 10:45:08
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 17:59:37
* @Last Modified time: 2026-03-04 10:41:35
*/
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { Space, List, Flex, Tooltip } from 'antd';
import { List, Flex, Tooltip, Form } from 'antd';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx'
@@ -31,7 +31,7 @@ interface SkillModalProps {
*
* A modal dialog for selecting skills from a searchable list.
* Features:
* - Search functionality to filter skills by search
* - Search functionality to filter skills by keywords
* - Grid layout displaying skill cards with icons and descriptions
* - Multi-select capability with visual feedback
* - Excludes already selected skills from the list
@@ -49,17 +49,19 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
const [visible, setVisible] = useState(false);
const [list, setList] = useState<Skill[]>([])
const [filterList, setFilterList] = useState<Skill[]>([])
const [query, setQuery] = useState<{search?: string}>({})
const [selectedIds, setSelectedIds] = useState<string[]>([])
const [selectedRows, setSelectedRows] = useState<Skill[]>([])
const [form] = Form.useForm()
const query = Form.useWatch([], form)
/**
* Closes the modal and resets all state
* Clears search query, selected IDs, and selected rows
*/
const handleClose = () => {
setVisible(false);
setQuery({})
form.resetFields()
setSelectedIds([])
setSelectedRows([])
};
@@ -70,7 +72,7 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
*/
const handleOpen = () => {
setVisible(true);
setQuery({})
form.resetFields()
setSelectedIds([])
setSelectedRows([])
};
@@ -82,7 +84,7 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
if (visible) {
getList()
}
}, [query.search, visible])
}, [query?.search, visible])
/**
* Fetches the skill list from API with current search parameters
@@ -96,6 +98,8 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
.then(res => {
const response = res as { items: Skill[] }
setList(response.items || [])
setSelectedIds([])
setSelectedRows([])
})
}
@@ -117,17 +121,6 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
handleClose
}));
/**
* Handles search input changes and resets selection
* Clears current selections when search query changes
* @param value - Search keyword
*/
const handleSearch = (value?: string) => {
setQuery({search: value})
setSelectedIds([])
setSelectedRows([])
}
/**
* Toggles skill selection state
* Adds skill to selection if not selected, removes if already selected
@@ -154,7 +147,7 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
if (list.length && selectedList.length) {
const unSelectedList = list.filter(item => selectedList.findIndex(vo => vo.id === item.id) < 0)
setFilterList([...unSelectedList])
} else if (list.length) {
} else {
setFilterList([...list])
}
}, [list, selectedList])
@@ -169,13 +162,16 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
onOk={handleSave}
width={1000}
>
<Space size={24} direction="vertical" className="rb:w-full">
<Flex gap={24} vertical>
{/* Search input for filtering skills */}
<SearchInput
placeholder={t('skills.searchPlaceholder')}
onSearch={handleSearch}
style={{ width: '100%' }}
/>
<Form form={form}>
<Form.Item name="search" noStyle>
<SearchInput
placeholder={t('skills.searchPlaceholder')}
className="rb:w-full!"
/>
</Form.Item>
</Form>
{/* Display empty state or skill grid */}
{filterList.length === 0
? <Empty />
@@ -191,9 +187,9 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
})} onClick={() => handleSelect(item)}>
<Flex>
{/* Skill avatar showing first letter of name */}
<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]">
<Flex align="center" justify="center" className="rb:size-12 rb:rounded-lg rb:mr-3.25! rb:bg-[#155eef] rb:text-[28px] rb:text-white">
{item.name[0]}
</div>
</Flex>
{/* Skill name and description */}
<div className="rb:flex-1 rb:max-w-[calc(100%-60px)]">
<div className="rb:font-medium rb:wrap-break-word rb:line-clamp-1">{item.name}</div>
@@ -207,7 +203,7 @@ const SkillListModal = forwardRef<SkillModalRef, SkillModalProps>(({
)}
/>
}
</Space>
</Flex>
</RbModal>
</>
);

View File

@@ -2,12 +2,12 @@
* @Author: ZhaoYing
* @Date: 2026-02-05 10:43:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-05 11:10:01
* @Last Modified time: 2026-02-25 15:36:14
*/
import { type FC, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Space, Button, Form, Flex, Tooltip, Checkbox } from 'antd'
import { CloseOutlined, CheckCircleFilled } from '@ant-design/icons'
import { CheckCircleFilled } from '@ant-design/icons'
import type {
SkillConfigForm,
@@ -15,7 +15,6 @@ import type {
} from './types'
import Empty from '@/components/Empty'
import SkillListModal from './SkillListModal'
import Card from '../Card'
import RbAlert from '@/components/RbAlert'
import type { Skill } from '@/views/Skills/types'
@@ -86,20 +85,19 @@ const SkillsItem: FC<SkillsItemProps> = ({
}, [allSkills])
return (
<Card
title={title}
extra={
<Space>
<div>
<Flex align="center" justify="space-between" className="rb:mb-2!">
<div className="rb:text-[#212332] rb:font-medium rb:leading-4.5 rb:px-1">{title}</div>
<Space size={16}>
{/* "Allow all skills" checkbox - only shown if supportAll is true */}
{supportAll && <Form.Item name={[...parentName, 'all_skills']} valuePropName="checked" noStyle>
<Checkbox>{t('application.allSkill')}</Checkbox>
<Checkbox className="rb:text-[12px]!">{t('application.allSkill')}</Checkbox>
</Form.Item>}
{/* Add skill button - disabled when all skills are enabled */}
<Button disabled={allSkills} style={{ padding: '0 8px', height: '24px' }} onClick={handleAddSkill}>+ {t('application.addSkill')}</Button>
<Button disabled={allSkills} type="link" className="rb:h-4! rb:p-0! rb:font-medium! rb:text-[12px]! rb:text-[#212332]" onClick={handleAddSkill}>+ {t('application.addSkill')}</Button>
</Space>
}
variant="borderL"
>
</Flex>
{/* Show alert when all skills enabled, otherwise show skill list */}
{allSkills
? <RbAlert color="green" icon={<CheckCircleFilled />}>{t('application.allSkillIntro')}</RbAlert>
@@ -111,39 +109,29 @@ const SkillsItem: FC<SkillsItemProps> = ({
<Empty size={88} subTitle={emptyTitle} />
) : (
/* Render list of configured skills */
<Space direction="vertical" size={12} className="rb:w-full">
<Flex vertical gap={12}>
{fields.map((field) => {
const skill = form.getFieldValue([...parentName, 'skill_ids', field.name])
return (
/* Individual skill card */
<div key={field.key} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<Flex key={field.key} align="center" justify="space-between" className="rb:p-3! rb:bg-[#FFFFFF] rb-border rb:rounded-lg">
<Flex className="rb:flex-1 rb:max-w-[calc(100%-186px)]!">
{/* Skill icon or fallback initial */}
{skill.icon
? <img src={skill.icon} className="rb:mr-3.25 rb:size-12 rb:rounded-lg" />
: <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]">
{skill.name?.[0]}
</div>
}
{/* Skill name and description */}
<div className="rb:flex-1 rb:max-w-[calc(100%-60px)]">
<div className="rb:font-medium rb:wrap-break-word rb:line-clamp-1">{skill.name}</div>
<div className="rb:font-medium rb:text-[#212332] rb:leading-5 rb:wrap-break-word rb:line-clamp-1">{skill.name}</div>
<Tooltip title={skill.description}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{skill.description}</div>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:mt-1 rb:wrap-break-word rb:line-clamp-1">{skill.description}</div>
</Tooltip>
</div>
</Flex>
<Space size={16} align="center">
{/* Remove skill button */}
<CloseOutlined
className="rb:cursor-pointer rb:text-[#5B6167] hover:rb:text-[#155EEF]"
onClick={() => remove(field.name)}
/>
</Space>
</div>
<div
className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => remove(field.name)}
></div>
</Flex>
)
})}
</Space>
</Flex>
)
)}
</Form.List>
@@ -155,7 +143,7 @@ const SkillsItem: FC<SkillsItemProps> = ({
selectedList={form.getFieldValue([...parentName, 'skill_ids']) || []}
refresh={refresh}
/>
</Card>
</div>
)
}
export default SkillsItem

View File

@@ -1,17 +1,18 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-05 10:42:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-05 10:42:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-26 10:18:56
*/
import { useEffect, type FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Space, Button, Switch, Form, Flex } from 'antd'
import { Space, Switch, Form, Flex } from 'antd'
import clsx from 'clsx'
import type {
SkillConfigForm,
} from './types'
import Card from '../Card'
import RbCard from '@/components/RbCard/Card'
import SkillsItem from './SkillsItem'
import { getSkillList } from '@/api/skill'
import type { Skill } from '@/views/Skills/types'
@@ -94,15 +95,15 @@ const SkillList: FC<{value?: SkillConfigForm; onChange?: (config: SkillConfigFor
return (
<Card
<RbCard
title={<>
{t('application.skill')}
<span className="rb:font-regular rb:text-[12px] rb:text-[#5B6167]"> ({t('application.skillTitle')})</span>
<div className="rb:font-[MiSans-Bold] rb:font-bold">{t('application.skill')}</div>
<div className="rb:font-regular! rb:text-[12px] rb:text-[#5B6167]"> {t('application.skillTitle')}</div>
</>}
extra={
<Space>
{/* Help button for skill configuration guidance */}
<Button style={{ padding: '0 8px', height: '24px' }}>{t('application.skillHelp')}</Button>
{/* <Button style={{ padding: '0 8px', height: '24px' }}>{t('application.skillHelp')}</Button> */}
{/* Toggle switch to enable/disable skill functionality */}
<Form.Item
valuePropName="checked"
@@ -113,9 +114,28 @@ const SkillList: FC<{value?: SkillConfigForm; onChange?: (config: SkillConfigFor
</Form.Item>
</Space>
}
headerType="borderless"
headerClassName={clsx("rb:py-[16px]! rb:leading-[22px]! rb:font-regular", {
'rb:h-[76px]! rb:py-[16px]!': !skillConfig?.enabled,
'rb:h-[68px]! rb:pb-2!': skillConfig?.enabled,
})}
>
{/* Render skill configuration UI only when enabled */}
{skillConfig?.enabled && <Flex vertical gap={12}>
{skillConfig?.enabled && <Flex vertical gap={8} className="rb:bg-[#FAFAFA] rb:rounded-xl rb:pt-2.5! rb:pb-3! rb:px-3!">
<div className="rb:text-[#212332] rb:font-medium rb:leading-4.5 rb:px-1">{t('application.executeProcessPreview')}</div>
<Flex align="center" justify="space-between" gap={14} className="rb:text-[12px] rb:bg-[#FFFFFF]! rb:rounded-lg rb-border rb:py-2.5! rb:pl-4! rb:pr-3.25! rb:mb-2!">
{/* Render each step in the process flow with numbered badges */}
{processObj.map((key, index) => (<>
<Flex align="center" gap={8}>
{/* Step number badge */}
<Flex align="center" justify="center" className="rb:size-4 rb:rounded-full rb:bg-[#171719] rb:text-white rb:font-medium">{index + 1}</Flex>
{/* Step label */}
<span className="rb:inline-block rb:max-w-16">{t(`application.${key}`)}</span>
</Flex>
{/* Arrow separator between steps (except after last step) */}
{index !== processObj.length - 1 && <div className="rb:w-10 rb:h-4.5 rb:bg-cover rb:bg-[url('@/assets/images/application/arrow_right.svg')]"></div>}
</>))}
</Flex>
{/* Dynamic skill binding configuration section */}
<Form.Item noStyle>
<SkillsItem
@@ -125,27 +145,8 @@ const SkillList: FC<{value?: SkillConfigForm; onChange?: (config: SkillConfigFor
emptyTitle={t('application.dynamicBindingSkill_empty')}
/>
</Form.Item>
{/* Execution process preview card showing workflow steps */}
<Card
title={t('application.executeProcessPreview')}
variant="borderL"
>
<Flex align="center" gap={8} className="rb:text-[12px]">
{/* Render each step in the process flow with numbered badges */}
{processObj.map((key, index) => (<>
<Flex align="center" gap={8} className="rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:bg-white rb:p-2!">
{/* Step number badge */}
<div className="rb:size-4 rb:rounded-full rb:bg-[#155EEF] rb:text-white rb:flex rb:items-center rb:justify-center rb:font-medium">{index + 1}</div>
{/* Step label */}
<span>{t(`application.${key}`)}</span>
</Flex>
{/* Arrow separator between steps (except after last step) */}
{index !== processObj.length - 1 && <div className="rb:text-[#8C9196]"></div>}
</>))}
</Flex>
</Card>
</Flex>}
</Card>
</RbCard>
)
}
export default SkillList

View File

@@ -16,7 +16,7 @@ import { type FC, type ReactNode } from 'react'
*/
export interface TagProps {
/** Tag color scheme */
color?: 'processing' | 'warning' | 'default' | 'success';
color?: 'processing' | 'warning' | 'default' | 'success' | 'dark';
/** Tag content */
children: ReactNode;
/** Additional CSS classes */
@@ -31,6 +31,7 @@ const colors = {
warning: 'rb:text-[#FF5D34] rb:border-[rgba(255,93,52,0.08)] rb:bg-[rgba(255,93,52,0.08)]',
default: 'rb:text-[#5B6167] rb:border-[rgba(91,97,103,0.30)] rb:bg-[rgba(91,97,103,0.08)]',
success: 'rb:text-[#369F21] rb:border-[rgba(54,159,33,0.30)] rb:bg-[rgba(54,159,33,0.08)]',
dark: 'rb:text-[#171719] rb:border-[#171719] rb:bg-transparent'
}
/**

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 10:15:39
*/
/**
* Tool List Component
@@ -12,7 +12,7 @@
import { type FC, useRef, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Space, Button, List, Switch } from 'antd'
import { Space, Button, Switch, Flex } from 'antd'
import Card from '../Card'
import type {
@@ -131,32 +131,30 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
<Card
title={t('application.toolConfiguration')}
extra={
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddTool}>+ {t('application.addTool')}</Button>
<Button className="rb:h-6! rb:py-0! rb:px-2! rb:rounded-md! rb:text-[#21233" onClick={handleAddTool}>+ {t('application.addTool')}</Button>
}
>
<div className="rb:leading-4.5 rb:text-[12px] rb:mb-2 rb:font-medium">
{t('application.toolManagement')}
</div>
{toolList.length === 0
? <Empty size={88} />
:
<List
grid={{ gutter: 12, column: 1 }}
dataSource={toolList}
renderItem={(item, index) => (
<List.Item>
<div key={index} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<div className="rb:font-medium rb:leading-4">
{item.label}
</div>
<Space size={12}>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteTool(index)}
></div>
<Switch checked={item.enabled} onChange={() => handleChangeEnabled(index)} />
</Space>
</div>
</List.Item>
)}
/>
? <div className="rb-border rb:rounded-xl rb:pt-4 rb:pb-6"><Empty size={88} /></div>
: <Flex vertical gap={12}>
{toolList.map((item, index) => (
<Flex key={index} align="center" justify="space-between" className="rb:py-2.5! rb:pl-4! rb:pr-3! rb-border rb:rounded-lg">
<div className="rb:font-medium rb:leading-4">
{item.label}
</div>
<Space size={12}>
<Switch size="small" checked={item.enabled} onChange={() => handleChangeEnabled(index)} />
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteTool(index)}
></div>
</Space>
</Flex>
))}
</Flex>
}
<ToolModal
ref={toolModalRef}

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:27
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 16:26:27
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-24 11:47:46
*/
/**
* Variable Edit Modal
@@ -11,7 +11,7 @@
*/
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
import { Form, Input, Select, InputNumber, Checkbox, Tag, Divider, Button } from 'antd';
import { Form, Input, Select, InputNumber, Checkbox, Tag, Divider, Button, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ApiExtensionModalRef, Variable, VariableEditModalRef } from './types'
@@ -150,8 +150,8 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
label: t(`application.${key}`),
}))}
onChange={handleChangeType}
labelRender={(props) => <div className="rb:flex rb:justify-between rb:items-center">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></div>}
optionRender={(props) => <div className="rb:flex rb:justify-between rb:items-center">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></div>}
labelRender={(props) => <Flex align="center" justify="space-between">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></Flex>}
optionRender={(props) => <Flex align="center" justify="space-between">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></Flex>}
/>
</FormItem>
{/* Variable Name */}

View File

@@ -7,8 +7,8 @@
}
.tabs :global(.ant-tabs-tab) {
line-height: 20px;
padding-bottom: 18px;
padding-top: 10px;
padding-bottom: 22px;
padding-top: 22px;
}
.tabs :global(.ant-tabs-tab-active) {
font-weight: 500;

View File

@@ -1,8 +1,8 @@
/*
* @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-03 18:57:36
*/
import React, { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router-dom';
@@ -88,12 +88,14 @@ const ApplicationConfig: React.FC = () => {
appRef={application?.type === 'agent' ? agentRef : application?.type === 'multi_agent' ? clusterRef : application?.type === 'workflow' ? workflowRef : undefined}
workflowRef={workflowRef}
/>
{activeTab === 'arrangement' && application?.type === 'agent' && <Agent ref={agentRef} />}
{activeTab === 'arrangement' && application?.type === 'multi_agent' && <Cluster ref={clusterRef} />}
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} />}
{activeTab === 'api' && <Api application={application} />}
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
{activeTab === 'statistics' && <Statistics application={application} />}
<div className="rb:p-3 rb:max-h-[calc(100vh-65px)] rb:overflow-auto">
{activeTab === 'arrangement' && application?.type === 'agent' && <Agent ref={agentRef} />}
{activeTab === 'arrangement' && application?.type === 'multi_agent' && <Cluster ref={clusterRef} />}
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} />}
{activeTab === 'api' && <Api application={application} />}
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
{activeTab === 'statistics' && <Statistics application={application} />}
</div>
</>
);
};

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-28 16:40:30
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-03 18:55:57
*/
import type { KnowledgeConfig } from './components/Knowledge/types'
import type { Variable } from './components/VariableList/types'
@@ -378,6 +378,8 @@ export interface StatisticsItem {
count: number;
/** Date string */
date: string;
/** Index signature for compatibility with ChartData */
[key: string]: string | number;
}
/**

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-04 10:44:29
*/
/**
* Application Management Page
@@ -10,21 +10,22 @@
* Supports creating, editing, and deleting applications
*/
import React, { useState, useRef, useEffect } from 'react';
import React, { useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Row, Col, App, Select, Space, Dropdown } from 'antd';
import { App, Select, Space, Form, Flex, Dropdown, Button } from 'antd';
import clsx from 'clsx';
import { DeleteOutlined } from '@ant-design/icons';
import { useSearchParams } from 'react-router-dom'
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 PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { formatDateTime } from '@/utils/format';
import UploadWorkflowModal from './components/UploadWorkflowModal'
import RbCard from '@/components/RbCard'
import RbButton from '@/components/RbButton'
import RbDescriptions from '@/components/RbDescriptions'
/**
* Application management main component
@@ -33,19 +34,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 [form] = Form.useForm()
const query = Form.useWatch([], form)
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 || null)
}, [searchParams])
/** Refresh application list */
@@ -79,14 +80,11 @@ const ApplicationManagement: React.FC = () => {
}
})
}
const handleChangeType = (value?: string) => {
setQuery(prev => ({...prev, type: value}))
}
const handleImport = () => {
uploadWorkflowModalRef.current?.handleOpen()
}
const handleClick = ({ key }: { key: string } ) => {
const handleClick = ({ key }: { key: string }) => {
switch (key) {
case 'thirdParty':
handleImport()
@@ -95,45 +93,48 @@ const ApplicationManagement: React.FC = () => {
}
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}>
<Form form={form} className="rb:mb-4!">
<Flex justify="space-between">
<Space size={10}>
<Form.Item name="type" noStyle>
<Select
placeholder={t('application.applicationType')}
options={[
{ value: null, label: t('application.allType') },
...types.map((type) => ({
value: type,
label: t(`application.${type}`),
}))
]}
className="rb:w-30!"
/>
</Form.Item>
<Form.Item name="search" noStyle>
<SearchInput
placeholder={t('application.searchPlaceholder')}
className="rb:w-75!"
/>
</Form.Item>
</Space>
<Space size={10}>
<Dropdown
menu={{ items: [
{ key: 'thirdParty', label: t('application.importWorkflow') },
], onClick: handleClick }}
menu={{
items: [
{ key: 'thirdParty', label: t('application.importWorkflow') },
], onClick: handleClick
}}
placement="bottomRight"
>
<Button>
{t('application.import')}
</Button>
</Dropdown>
<Button type="primary" onClick={handleCreate}>
<RbButton type="primary" icon={<div className="rb:size-3 rb:bg-cover rb:bg-[url('@/assets/images/common/plus.svg')]"></div>} onClick={handleCreate}>
{t('application.createApplication')}
</Button>
</RbButton>
</Space>
</Col>
</Row>
</Flex>
</Form>
<PageScrollList<Application, Query>
ref={scrollListRef}
@@ -142,20 +143,23 @@ const ApplicationManagement: React.FC = () => {
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>
}
avatarText={item.name.trim()[0]}
avatarClassName={clsx({
'rb:bg-[#155EEF]': item.type === 'agent',
'rb:bg-[#9C6FFF]!': item.type === 'multi_agent',
'rb:bg-[#171719]': item.type === 'workflow',
})}
footer={<Flex justify="space-between" gap={12}>
<RbButton danger className="rb:w-22.25" onClick={() => handleDelete(item)}>{t('common.delete')}</RbButton>
<RbButton type="primary" ghost className="rb:flex-1" onClick={() => handleEdit(item)}>{t('application.configuration')}</RbButton>
</Flex>}
>
{['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',
<RbDescriptions
items={['type', 'source', 'created_at'].map(key => ({
key,
label: t(`application.${key}`),
children: <span className={clsx('rb:font-medium', {
'rb:text-[#155EEF]': key === 'type',
})}>
{key === 'source' && item.is_shared
? t('application.shared')
@@ -166,13 +170,8 @@ const ApplicationManagement: React.FC = () => {
: 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>
)}
/>