feat(web): app page ui upgrade
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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[]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
98
web/src/views/ApplicationConfig/components/ChartCard.tsx
Normal file
98
web/src/views/ApplicationConfig/components/ChartCard.tsx
Normal 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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user