feat(web): app page ui upgrade
This commit is contained in:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user