feat(web): app page ui upgrade

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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