feat: Add base project structure with API and web components
This commit is contained in:
81
web/src/views/ApplicationConfig/components/AiPromptModal.tsx
Normal file
81
web/src/views/ApplicationConfig/components/AiPromptModal.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Row, Col, Space, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { AiPromptModalRef } from '../types'
|
||||
// import { request } from '@/utils/request'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import Markdown from '@/components/Markdown';
|
||||
|
||||
interface AiPromptModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
// refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
// const handleSave = () => {
|
||||
// }
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.AIPromptAssistant')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={1000}
|
||||
>
|
||||
<Row className="rb:rounded-[12px] rb:border rb:border-[#DFE4ED]">
|
||||
<Col span={12} className="rb:border-r rb:border-[#DFE4ED]">
|
||||
<div className="rb:p-[12px_17px] rb:border-b rb:border-[#DFE4ED]">{t('application.generatedPrompt')}</div>
|
||||
<div className="rb:h-[200px] rb:p-[16px]">
|
||||
<div className="rb:bg-[#F0F3F8] rb:h-full rb:w-full">
|
||||
<Markdown
|
||||
content={content}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="rb:p-[12px_17px] rb:border-b rb:border-[#DFE4ED]">{t('application.conversationOptimizationPrompt')}</div>
|
||||
<div className="rb:h-[200px] rb:p-[16px]">
|
||||
<div className="rb:bg-[#F0F3F8] rb:h-full rb:w-full">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12} className="rb:border-r rb:border-[#DFE4ED]">
|
||||
<Space>
|
||||
<Button>{t('common.copy')}</Button>
|
||||
<Button type="primary">{t('common.apply')}</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
</Col>
|
||||
</Row>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default AiPromptModal;
|
||||
@@ -0,0 +1,96 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ApiExtensionModalData, ApiExtensionModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface ApiExtensionModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const ApiExtensionModal = forwardRef<ApiExtensionModalRef, ApiExtensionModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ApiExtensionModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
console.log('values', values)
|
||||
refresh()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.addApiExtension')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('application.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="apiEndpoint"
|
||||
label={t('application.apiEndpoint')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="apiKey"
|
||||
label={t('application.apiKey')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiExtensionModal;
|
||||
27
web/src/views/ApplicationConfig/components/Card.tsx
Normal file
27
web/src/views/ApplicationConfig/components/Card.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
|
||||
interface CardProps {
|
||||
title?: string | ReactNode;
|
||||
children: ReactNode;
|
||||
extra?: ReactNode;
|
||||
}
|
||||
|
||||
const Card: FC<CardProps> = ({
|
||||
title,
|
||||
children,
|
||||
extra,
|
||||
}) => {
|
||||
return (
|
||||
<RbCard
|
||||
title={title}
|
||||
extra={extra}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]! rb:before:h-[19px]"
|
||||
>
|
||||
{children}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
337
web/src/views/ApplicationConfig/components/Chat.tsx
Normal file
337
web/src/views/ApplicationConfig/components/Chat.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import { type FC, useRef, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
import { Input, Form } from 'antd'
|
||||
import ChatIcon from '@/assets/images/application/chat.svg'
|
||||
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
|
||||
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.svg'
|
||||
import type { ChatItem, ChatData, Config } from '../types'
|
||||
import { runCompare, draftRun } from '@/api/application'
|
||||
import Empty from '@/components/Empty'
|
||||
import Markdown from '@/components/Markdown'
|
||||
|
||||
interface ChatProps {
|
||||
chatList: ChatData[];
|
||||
data: Config;
|
||||
updateChatList: (list: ChatData[]) => void;
|
||||
handleSave: (flag?: boolean) => Promise<any>;
|
||||
source?: 'cluster' | 'agent';
|
||||
}
|
||||
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm<{ message: string }>()
|
||||
const scrollContainerRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isCluster, setIsCluster] = useState(source === 'cluster')
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [compareLoading, setCompareLoading] = useState(false)
|
||||
|
||||
// 当聊天列表更新时,自动滚动到底部
|
||||
useEffect(() => {
|
||||
// 延迟一下,确保DOM已经更新
|
||||
setTimeout(() => {
|
||||
scrollContainerRefs.current.forEach(container => {
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}, [chatList]);
|
||||
useEffect(() => {
|
||||
setIsCluster(source === 'cluster')
|
||||
}, [source])
|
||||
|
||||
const handleSend = () => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
setCompareLoading(true)
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
const message = form.getFieldValue('message')
|
||||
if (!message || message.trim() === '') return
|
||||
const newUserMessage: ChatItem = {
|
||||
role: 'question',
|
||||
content: message,
|
||||
time: Date.now(),
|
||||
};
|
||||
updateChatList((prev: ChatData[]) => {
|
||||
return prev.map(item => ({
|
||||
...item,
|
||||
list: [
|
||||
...(item.list || []),
|
||||
newUserMessage
|
||||
]
|
||||
}))
|
||||
})
|
||||
form.setFieldsValue({ message: undefined })
|
||||
// 添加空的助手消息用于流式更新
|
||||
const assistantMessages: Record<string, ChatItem> = {};
|
||||
if (isCluster) {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'answer',
|
||||
content: '',
|
||||
time: Date.now(),
|
||||
};
|
||||
assistantMessages['cluster'] = assistantMessage;
|
||||
updateChatList((prev: ChatData[]) => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
} else {
|
||||
chatList.forEach(item => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'answer',
|
||||
content: '',
|
||||
time: Date.now(),
|
||||
};
|
||||
assistantMessages[item.model_config_id] = assistantMessage;
|
||||
});
|
||||
updateChatList((prev: ChatData[]) => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessages[item.model_config_id]]
|
||||
})))
|
||||
}
|
||||
|
||||
const handleStreamMessage = (data: string) => {
|
||||
setCompareLoading(false)
|
||||
try {
|
||||
const lines = data.split('\n');
|
||||
let currentEvent = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.substring(6).trim();
|
||||
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_message')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.content && parsed.model_config_id) {
|
||||
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: msg.content + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'message')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
if (parsed.content) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: (msg.content || '') + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_end')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.message_length === 0 && parsed.model_config_id) {
|
||||
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: null };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'model_end')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
if (parsed.message_length === 0) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: null };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (currentEvent === 'compare_end') {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse stream data error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
if (isCluster) {
|
||||
draftRun(data.app_id, { message, conversation_id: conversationId, stream: true }, handleStreamMessage)
|
||||
.finally(() => setLoading(false))
|
||||
} else {
|
||||
runCompare(data.app_id, {
|
||||
message,
|
||||
models: chatList.map(item => ({
|
||||
model_config_id: item.model_config_id,
|
||||
label: item.label,
|
||||
model_parameters: item.model_parameters,
|
||||
conversation_id: item.conversation_id
|
||||
})),
|
||||
variables: {},
|
||||
"parallel": true,
|
||||
"stream": true,
|
||||
"timeout": 60,
|
||||
}, handleStreamMessage)
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
})
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
updateChatList(chatList.filter((_, voIndex) => voIndex !== index))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:relative rb:h-[calc(100vh-110px)]">
|
||||
{chatList.length === 0
|
||||
? <Empty
|
||||
url={DebuggingEmpty}
|
||||
title={t('application.debuggingEmpty')}
|
||||
subTitle={t('application.debuggingEmptyDesc')}
|
||||
className="rb:h-full"
|
||||
/>
|
||||
: <>
|
||||
<div className={clsx(`rb:grid rb:grid-cols-${chatList.length} rb:overflow-hidden rb:w-full`, {
|
||||
'rb:h-[calc(100vh-236px)]': !isCluster,
|
||||
'rb:h-[calc(100%-76px)]': isCluster,
|
||||
})}>
|
||||
{chatList.map((chat, index) => (
|
||||
<div key={index} className={clsx('rb:h-full 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-[12px]': index === chatList.length - 1,
|
||||
'rb:rounded-tl-[12px]': 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-[16px] rb:h-[16px] rb:cursor-pointer rb:absolute rb:top-[12px] rb:right-[12px] rb:cursor-pointer 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>
|
||||
}
|
||||
{!chat.list || chat.list.length === 0
|
||||
? <Empty url={ChatIcon} title={t('application.chatEmpty')} className="rb:h-full" />
|
||||
: (
|
||||
<div ref={el => scrollContainerRefs.current[index] = el} className={clsx(`rb:relative rb:overflow-y-auto rb:overflow-x-hidden`, {
|
||||
'rb:h-[calc(100vh-186px)]': isCluster,
|
||||
'rb:h-[calc(100vh-286px)]': !isCluster,
|
||||
})}>
|
||||
{chat.list?.map((vo, voIndex) => {
|
||||
if (compareLoading && voIndex === chat.list?.length - 1) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div key={voIndex} className={clsx("rb:relative rb:mt-[24px]", {
|
||||
'rb:right-[16px] rb:text-right': vo.role === 'question',
|
||||
'rb:left-[16px] rb:text-left': vo.role !== 'question',
|
||||
})}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-[16px] rb:font-regular">{vo.role === 'question' ? 'You' : chat.label}</div>
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-[8px] rb:mt-[6px] rb:leading-[18px] rb:p-[10px_12px_2px_12px] rb:inline-block', {
|
||||
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': vo.role !== 'question' && vo.content === null,
|
||||
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': vo.role === 'question' && vo.content,
|
||||
'rb:bg-[#ffffff] rb:border-[rgba(235,235,235,1)]': vo.role !== 'question' && (vo.content || vo.content === ''),
|
||||
'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,
|
||||
})}>
|
||||
<Markdown content={vo.content === null ? t('application.ReplyException') : vo.content} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-[10px] rb:p-[16px]">
|
||||
<Form form={form} style={{width: 'calc(100% - 54px)'}}>
|
||||
<Form.Item name="message" className="rb:mb-[0]!">
|
||||
<Input
|
||||
className="rb:h-[44px] rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
placeholder={t('application.chatPlaceholder')}
|
||||
onPressEnter={handleSend}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<img src={ChatSendIcon} className={clsx("rb:w-[44px] rb:h-[44px] rb:cursor-pointer", {
|
||||
'rb:opacity-50': loading,
|
||||
})} onClick={handleSend} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Chat;
|
||||
131
web/src/views/ApplicationConfig/components/ConfigHeader.tsx
Normal file
131
web/src/views/ApplicationConfig/components/ConfigHeader.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { type FC, useRef } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Layout, Tabs, Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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'
|
||||
import deleteIcon from '@/assets/images/delete_hover.svg'
|
||||
import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types';
|
||||
import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal'
|
||||
import type { CopyModalRef } from '../types'
|
||||
import { deleteApplication } from '@/api/application'
|
||||
import CopyModal from './CopyModal'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
const tabKeys = ['arrangement', 'api', 'release']
|
||||
const menuIcons: Record<string, string> = {
|
||||
edit: editIcon,
|
||||
copy: copyIcon,
|
||||
export: exportIcon,
|
||||
delete: deleteIcon
|
||||
}
|
||||
interface ConfigHeaderProps {
|
||||
application?: Application;
|
||||
activeTab: string;
|
||||
handleChangeTab: (key: string) => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleChangeTab, refresh }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const applicationModalRef = useRef<ApplicationModalRef>(null);
|
||||
const copyModalRef = useRef<CopyModalRef>(null);
|
||||
|
||||
const formatTabItems = () => {
|
||||
return tabKeys.map(key => ({
|
||||
key,
|
||||
label: t(`application.${key}`),
|
||||
}))
|
||||
}
|
||||
const formatMenuItems = () => {
|
||||
const items = ['edit', 'copy', 'delete'].map(key => ({
|
||||
key,
|
||||
icon: <img src={menuIcons[key]} className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]" />,
|
||||
label: t(`common.${key}`),
|
||||
}))
|
||||
return {
|
||||
items,
|
||||
onClick: handleClick
|
||||
}
|
||||
}
|
||||
const handleClick: MenuProps['onClick'] = ({ key }) => {
|
||||
console.log('key', key)
|
||||
switch (key) {
|
||||
case 'edit':
|
||||
applicationModalRef.current?.handleOpen(application as Application)
|
||||
break;
|
||||
case 'copy':
|
||||
copyModalRef.current?.handleOpen()
|
||||
break;
|
||||
case 'export':
|
||||
break;
|
||||
case 'delete':
|
||||
handleDelete()
|
||||
break;
|
||||
}
|
||||
}
|
||||
const handleDelete = () => {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
deleteApplication(id as string)
|
||||
.then(() => {
|
||||
goToApplication()
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('Failed to delete application');
|
||||
});
|
||||
}
|
||||
const goToApplication = () => {
|
||||
navigate('/application', { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header className="rb:w-full rb:h-[64px] rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-[32px]">
|
||||
<div className="rb:h-[32px] rb:flex rb:items-center rb:font-medium">
|
||||
<div className="rb:w-[32px] rb:h-[32px] rb:rounded-[8px] rb:mr-[13px] 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={formatMenuItems()}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<div
|
||||
className="rb:w-[20px] rb:h-[20px] 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}
|
||||
className={styles.tabs}
|
||||
/>
|
||||
</div>
|
||||
<div className="rb:h-[32px] rb:flex rb:items-center rb:justify-end rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
|
||||
<img src={logoutIcon} className="rb:mr-[8px]" />
|
||||
{t('application.returnToApplicationList')}
|
||||
</div>
|
||||
</Header>
|
||||
<ApplicationModal
|
||||
ref={applicationModalRef}
|
||||
refresh={refresh}
|
||||
/>
|
||||
<CopyModal ref={copyModalRef} data={application as Application} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigHeader;
|
||||
87
web/src/views/ApplicationConfig/components/CopyModal.tsx
Normal file
87
web/src/views/ApplicationConfig/components/CopyModal.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import type { CopyModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { copyApplication } from '@/api/application'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface CopyModalProps {
|
||||
data: Application
|
||||
}
|
||||
|
||||
const CopyModal = forwardRef<CopyModalRef, CopyModalProps>(({
|
||||
data
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
setVisible(false);
|
||||
setLoading(true)
|
||||
const values = form.getFieldsValue()
|
||||
copyApplication(data.id, values.new_name)
|
||||
.then((res) => {
|
||||
const resData = res as Application
|
||||
navigate(`/application/config/${resData.id}`)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<RbModal
|
||||
title={t('application.copyApplication')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.copy')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
{/* 应用名 */}
|
||||
<FormItem
|
||||
name="new_name"
|
||||
label={t('application.applicationName')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default CopyModal;
|
||||
160
web/src/views/ApplicationConfig/components/Knowledge.tsx
Normal file
160
web/src/views/ApplicationConfig/components/Knowledge.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { type FC, useRef, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, List } from 'antd'
|
||||
import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg'
|
||||
import Card from './Card'
|
||||
import type {
|
||||
KnowledgeConfigForm,
|
||||
KnowledgeConfig,
|
||||
RerankerConfig,
|
||||
KnowledgeBase,
|
||||
KnowledgeModalRef,
|
||||
KnowledgeConfigModalRef,
|
||||
KnowledgeGlobalConfigModalRef,
|
||||
} from '../types'
|
||||
import Empty from '@/components/Empty'
|
||||
import KnowledgeListModal from './KnowledgeListModal'
|
||||
import KnowledgeConfigModal from './KnowledgeConfigModal'
|
||||
import KnowledgeGlobalConfigModal from './KnowledgeGlobalConfigModal'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig) => void}> = ({data, onUpdate}) => {
|
||||
const { t } = useTranslation()
|
||||
const knowledgeModalRef = useRef<KnowledgeModalRef>(null)
|
||||
const knowledgeConfigModalRef = useRef<KnowledgeConfigModalRef>(null)
|
||||
const knowledgeGlobalConfigModalRef = useRef<KnowledgeGlobalConfigModalRef>(null)
|
||||
const [knowledgeList, setKnowledgeList] = useState<KnowledgeBase[]>([])
|
||||
const [editConfig, setEditConfig] = useState<KnowledgeConfig>({} as KnowledgeConfig)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setEditConfig({ ...(data || {}) })
|
||||
const knowledge_bases = [...(data.knowledge_bases || [])]
|
||||
setKnowledgeList(knowledge_bases)
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
knowledge_bases: knowledge_bases,
|
||||
}))
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const handleKnowledgeConfig = () => {
|
||||
knowledgeGlobalConfigModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleAddKnowledge = () => {
|
||||
knowledgeModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleDeleteKnowledge = (id: string) => {
|
||||
const list = knowledgeList.filter(item => item.id !== id)
|
||||
setKnowledgeList([...list])
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
knowledge_bases: [...list],
|
||||
}))
|
||||
}
|
||||
const handleEditKnowledge = (item: KnowledgeBase) => {
|
||||
knowledgeConfigModalRef.current?.handleOpen(item)
|
||||
}
|
||||
const refresh = (values: KnowledgeBase[] | KnowledgeConfigForm | RerankerConfig, type: 'knowledge' | 'knowledgeConfig' | 'rerankerConfig') => {
|
||||
if (type === 'knowledge') {
|
||||
let list = [...knowledgeList]
|
||||
if (list.length > 0) {
|
||||
(Array.isArray(values) ? values : [values]).forEach(vo => {
|
||||
const index = list.findIndex(item => item.id === (vo as KnowledgeBase).id)
|
||||
if (index === -1) {
|
||||
list.push(vo as KnowledgeBase)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
list = [...values as KnowledgeBase[]]
|
||||
}
|
||||
setKnowledgeList([...list])
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
knowledge_bases: [...list],
|
||||
}))
|
||||
} else if (type === 'knowledgeConfig') {
|
||||
const index = knowledgeList.findIndex(item => item.id === (values as KnowledgeBase).kb_id)
|
||||
const list = [...knowledgeList]
|
||||
list[index] = {
|
||||
...list[index],
|
||||
config: {...values as KnowledgeConfigForm}
|
||||
}
|
||||
setKnowledgeList([...list])
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
knowledge_bases: [...list],
|
||||
}))
|
||||
} else if (type === 'rerankerConfig') {
|
||||
setEditConfig(prev => ({ ...prev, ...(values as RerankerConfig) }))
|
||||
onUpdate(prev => ({
|
||||
...prev,
|
||||
...values,
|
||||
reranker_id: values.rerank_model ? values.reranker_id : undefined,
|
||||
reranker_top_k: values.rerank_model ? values.reranker_top_k : undefined,
|
||||
}))
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
title={t('application.knowledgeBaseAssociation')}
|
||||
extra={
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={() => handleKnowledgeConfig()}>{t('application.globalConfig')}</Button>
|
||||
}
|
||||
>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-[12px]">
|
||||
<div className="rb:font-medium rb:leading-[20px]">{t('application.associatedKnowledgeBase')}</div>
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAddKnowledge}>+{t('application.addKnowledgeBase')}</Button>
|
||||
</div>
|
||||
|
||||
{knowledgeList.length === 0
|
||||
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
|
||||
:
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={knowledgeList}
|
||||
renderItem={(item) => (
|
||||
<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-[8px]">
|
||||
<div className="rb:font-medium rb:leading-[16px]">
|
||||
{item.name}
|
||||
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-[8px]">
|
||||
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
|
||||
</Tag>
|
||||
<div className="rb:mt-[4px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[20px]">{t('application.contains', {include_count: item.doc_num})}</div>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:w-[24px] rb:h-[24px] 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-[24px] rb:h-[24px] rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteKnowledge(item.id)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
{/* 全局设置 */}
|
||||
<KnowledgeGlobalConfigModal
|
||||
data={editConfig}
|
||||
ref={knowledgeGlobalConfigModalRef}
|
||||
refresh={refresh}
|
||||
/>
|
||||
{/* 知识库列表 */}
|
||||
<KnowledgeListModal
|
||||
ref={knowledgeModalRef}
|
||||
selectedList={knowledgeList}
|
||||
refresh={refresh}
|
||||
/>
|
||||
<KnowledgeConfigModal
|
||||
ref={knowledgeConfigModalRef}
|
||||
refresh={refresh}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default Knowledge
|
||||
@@ -0,0 +1,180 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Select, InputNumber } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { KnowledgeConfigModalRef, KnowledgeBase, KnowledgeConfigForm } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import RbSlider from '@/components/RbSlider'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface KnowledgeConfigModalProps {
|
||||
refresh: (values: KnowledgeConfigForm, type: 'knowledgeConfig') => void;
|
||||
}
|
||||
const retrieveTypes = ['participle', 'semantic', 'hybrid']
|
||||
|
||||
const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfigModalProps>(({
|
||||
refresh,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<KnowledgeConfigForm>();
|
||||
const [data, setData] = useState<KnowledgeBase | null>(null);
|
||||
|
||||
const values = Form.useWatch<KnowledgeConfigForm>([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setData(null)
|
||||
};
|
||||
|
||||
const handleOpen = (data: KnowledgeBase) => {
|
||||
form.setFieldsValue({
|
||||
retrieve_type: retrieveTypes[0],
|
||||
kb_id: data.id,
|
||||
...(data || {}),
|
||||
...(data?.config || {}),
|
||||
})
|
||||
setData({...data})
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
refresh(values, 'knowledgeConfig')
|
||||
handleClose()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (values?.retrieve_type) {
|
||||
const initialValues = Object.keys(values).map(key => {
|
||||
return {
|
||||
[key as keyof KnowledgeConfigForm]: (key === 'kb_id' || key === 'retrieve_type') ? values[key] : undefined
|
||||
}
|
||||
})
|
||||
form.resetFields(initialValues)
|
||||
}
|
||||
}, [values?.retrieve_type])
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.knowledgeConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
{data && (
|
||||
<div className="rb:mb-[24px] rb:flex rb:items-center rb:justify-between rb:border rb:rounded-[8px] rb:p-[17px_16px] rb:cursor-pointer rb:bg-[#F0F3F8] rb:border-[#DFE4ED] rb:text-[#212332]">
|
||||
<div className="rb:text-[16px] rb:leading-[22px]">
|
||||
{data.name}
|
||||
<div className="rb:text-[12px] rb:leading-[16px] rb:text-[#5B6167] rb:mt-[8px]">{t('application.contains', {include_count: data.doc_num})}</div>
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:leading-[16px] rb:text-[#5B6167]">{formatDateTime(data.updated_at, 'YYYY-MM-DD HH:mm:ss')}</div>
|
||||
</div>
|
||||
)}
|
||||
<FormItem name="kb_id" hidden />
|
||||
{/* 检索模式 */}
|
||||
<FormItem
|
||||
name="retrieve_type"
|
||||
label={t('application.retrieve_type')}
|
||||
extra={t('application.retrieve_type_desc')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
|
||||
<Select
|
||||
options={retrieveTypes.map(key => ({
|
||||
label: t(`application.${key}`),
|
||||
value: key,
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* Top K */}
|
||||
<FormItem
|
||||
name="top_k"
|
||||
label={t('application.top_k')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
extra={t('application.top_k_desc')}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</FormItem>
|
||||
{/* 语义相似度阈值 similarity_threshold */}
|
||||
{values?.retrieve_type === 'semantic' && (
|
||||
<FormItem
|
||||
name="similarity_threshold"
|
||||
label={t('application.similarity_threshold')}
|
||||
extra={t('application.similarity_threshold_desc')}
|
||||
>
|
||||
<RbSlider
|
||||
max={1.0}
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
{/* 分词匹配度阈值 vector_similarity_weight */}
|
||||
{values?.retrieve_type === 'participle' && (
|
||||
<FormItem
|
||||
name="vector_similarity_weight"
|
||||
label={t('application.vector_similarity_weight')}
|
||||
extra={t('application.vector_similarity_weight_desc')}
|
||||
>
|
||||
<RbSlider
|
||||
max={1.0}
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
{/* 混合检索权重 */}
|
||||
{values?.retrieve_type === 'hybrid' && (
|
||||
<>
|
||||
<FormItem
|
||||
name="similarity_threshold"
|
||||
label={t('application.similarity_threshold')}
|
||||
extra={t('application.similarity_threshold_desc1')}
|
||||
>
|
||||
<RbSlider
|
||||
max={1.0}
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="vector_similarity_weight"
|
||||
label={t('application.vector_similarity_weight')}
|
||||
extra={t('application.vector_similarity_weight_desc1')}
|
||||
>
|
||||
<RbSlider
|
||||
max={1.0}
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default KnowledgeConfigModal;
|
||||
@@ -0,0 +1,121 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, InputNumber, Switch } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { RerankerConfig, KnowledgeGlobalConfigModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface KnowledgeGlobalConfigModalProps {
|
||||
data: RerankerConfig;
|
||||
refresh: (values: RerankerConfig, type: 'rerankerConfig') => void;
|
||||
}
|
||||
|
||||
const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, KnowledgeGlobalConfigModalProps>(({
|
||||
refresh,
|
||||
data,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<RerankerConfig>();
|
||||
const values = Form.useWatch<RerankerConfig>([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
form.setFieldsValue({ ...data, rerank_model: !!data?.reranker_id })
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
refresh(values, 'rerankerConfig')
|
||||
handleClose()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (values?.rerank_model) {
|
||||
form.setFieldsValue({ ...data })
|
||||
} else {
|
||||
form.setFieldsValue({ reranker_id: undefined, reranker_top_k: undefined })
|
||||
}
|
||||
}, [values?.rerank_model])
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.globalConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:mb-[24px]">{t('application.globalConfigDesc')}</div>
|
||||
|
||||
{/* 结果重排 */}
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:my-[24px]">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-[20px]">
|
||||
{t('application.rerankModel')}
|
||||
<div className="rb:mt-[4px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">{t('application.rerankModelDesc')}</div>
|
||||
</div>
|
||||
<FormItem
|
||||
name="rerank_model"
|
||||
valuePropName="checked"
|
||||
className="rb:mb-[0px]!"
|
||||
>
|
||||
<Switch />
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{values?.rerank_model && <>
|
||||
<FormItem
|
||||
name="reranker_id"
|
||||
label={t('application.rearrangementModel')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
extra={t('application.rearrangementModelDesc')}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* Top K */}
|
||||
<FormItem
|
||||
name="reranker_top_k"
|
||||
label={t('application.reranker_top_k')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
extra={t('application.reranker_top_k_desc')}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} min={1} max={20} />
|
||||
</FormItem>
|
||||
</>}
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default KnowledgeGlobalConfigModal;
|
||||
@@ -0,0 +1,147 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Space, List } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
import type { KnowledgeModalRef, KnowledgeBase } from '../types'
|
||||
import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { getKnowledgeBaseList } from '@/views/KnowledgeBase/service'
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import Empty from '@/components/Empty'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
interface KnowledgeModalProps {
|
||||
refresh: (rows: KnowledgeBase[], type: 'knowledge') => void;
|
||||
selectedList: KnowledgeBase[];
|
||||
}
|
||||
|
||||
const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
refresh,
|
||||
selectedList
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
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 handleClose = () => {
|
||||
setVisible(false);
|
||||
setQuery({})
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
setQuery({})
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
getList()
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query.keywords])
|
||||
const getList = () => {
|
||||
getKnowledgeBaseList(undefined, {
|
||||
...query,
|
||||
pagesize: 100,
|
||||
orderby:'created_at',
|
||||
desc:true,
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as { items: KnowledgeBaseListItem[] }
|
||||
setList(response.items || [])
|
||||
})
|
||||
}
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
refresh(selectedRows.map(item => ({
|
||||
...item,
|
||||
config: {
|
||||
similarity_threshold: 0.7,
|
||||
strategy: "hybrid",
|
||||
top_k: 3,
|
||||
weight: 1,
|
||||
}
|
||||
})), 'knowledge')
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery({keywords: value})
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
}
|
||||
const handleSelect = (item: KnowledgeBase) => {
|
||||
const index = selectedIds.indexOf(item.id)
|
||||
if (index === -1) {
|
||||
setSelectedIds([...selectedIds, item.id])
|
||||
setSelectedRows([...selectedRows, item])
|
||||
} else {
|
||||
setSelectedIds(selectedIds.filter(id => id !== item.id))
|
||||
setSelectedRows(selectedRows.filter(row => row.id !== item.id))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (list.length && selectedList.length) {
|
||||
const unSelectedList = list.filter(item => selectedList.findIndex(vo => vo.id === item.id) < 0)
|
||||
setFilterList([...unSelectedList])
|
||||
} else if (list.length) {
|
||||
setFilterList([...list])
|
||||
}
|
||||
}, [list, selectedList])
|
||||
|
||||
return (
|
||||
<>
|
||||
<RbModal
|
||||
title={t('application.chooseKnowledge')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
width={1000}
|
||||
>
|
||||
<Space size={24} direction="vertical" className="rb:w-full">
|
||||
<SearchInput
|
||||
placeholder={t('knowledgeBase.searchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{filterList.length === 0
|
||||
? <Empty />
|
||||
: <List
|
||||
grid={{ gutter: 16, column: 2 }}
|
||||
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-[8px] rb:p-[17px_16px] 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)}>
|
||||
<div className="rb:text-[16px] rb:leading-[22px]">
|
||||
{item.name}
|
||||
<div className="rb:text-[12px] rb:leading-[16px] rb:text-[#5B6167] rb:mt-[8px]">{t('application.contains', {include_count: item.doc_num})}</div>
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:leading-[16px] rb:text-[#5B6167]">{formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')}</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</Space>
|
||||
</RbModal>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default KnowledgeListModal;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Cascader } from 'antd'
|
||||
import type { CascaderProps } from 'antd';
|
||||
import { getModelProviderList } from '@/api/models'
|
||||
|
||||
interface Option {
|
||||
value?: string | number | null;
|
||||
label: React.ReactNode;
|
||||
children?: Option[];
|
||||
isLeaf?: boolean;
|
||||
}
|
||||
const CustomSelect: FC<CascaderProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
useEffect(() => {
|
||||
getProviderList()
|
||||
}, []);
|
||||
|
||||
const getProviderList = () => {
|
||||
getModelProviderList().then(res => {
|
||||
const response = res as string[]
|
||||
setOptions(response.map((key: string) => ({
|
||||
value: key,
|
||||
label: t(`model.${key}`),
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
})))
|
||||
})
|
||||
}
|
||||
const loadData = (selectedOptions: Option[]) => {
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
console.log(targetOption)
|
||||
}
|
||||
return (
|
||||
<Cascader
|
||||
options={options}
|
||||
loadData={loadData}
|
||||
changeOnSelect
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default CustomSelect;
|
||||
147
web/src/views/ApplicationConfig/components/ModelConfigModal.tsx
Normal file
147
web/src/views/ApplicationConfig/components/ModelConfigModal.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ModelConfig, ModelConfigModalRef, Config, ChatData } from '../types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import RbSlider from '@/components/RbSlider'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface ModelConfigModalProps {
|
||||
modelList: Model[];
|
||||
refresh: (values: ModelConfig, type: 'model') => void;
|
||||
data: Config;
|
||||
chatList: ChatData[]
|
||||
}
|
||||
|
||||
const configFields = [
|
||||
{ key: 'temperature', max: 2, min: 0, step: 0.1, defaultValue: 0.7 },
|
||||
{ key: 'max_tokens', max: 32000, min: 256, step: 1, defaultValue: 2000 },
|
||||
{ key: 'top_p', max: 1, min: 0, step: 0.1, defaultValue: 1.0 },
|
||||
{ key: 'frequency_penalty', max: 2.0, min: -2.0, step: 0.1, defaultValue: 0.0 },
|
||||
{ key: 'presence_penalty', max: 2.0, min: -2.0, step: 0.1, defaultValue: 0.0 },
|
||||
{ key: 'n', max: 10, min: 1, step: 1, defaultValue: 1 },
|
||||
]
|
||||
|
||||
const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(({
|
||||
refresh,
|
||||
data,
|
||||
modelList
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ModelConfig>();
|
||||
const [source, setSource] = useState<'chat' | 'model'>('model')
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleOpen = (source: 'chat' | 'model', model) => {
|
||||
setSource(source)
|
||||
if (source === 'model') {
|
||||
form.setFieldsValue({
|
||||
...(data?.model_parameters || {}),
|
||||
default_model_config_id: data.default_model_config_id || ''
|
||||
})
|
||||
} else if (source === 'chat') {
|
||||
if (model) {
|
||||
form.setFieldsValue({
|
||||
...(model?.model_parameters || {}),
|
||||
default_model_config_id: model.default_model_config_id || ''
|
||||
})
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
...(data?.model_parameters || {}),
|
||||
default_model_config_id: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
refresh(values, source)
|
||||
handleClose()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
const handleChange = (value: string, option: Model) => {
|
||||
if (source === 'chat') {
|
||||
form.setFieldValue('label', option.name)
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({...(data?.model_parameters || {})})
|
||||
}, [values?.default_model_config_id])
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.modelConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
cancelText={t('application.resetDefault')}
|
||||
okText={t('application.apply')}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
className="rb:ml-[7px]!"
|
||||
>
|
||||
<FormItem
|
||||
name="default_model_config_id"
|
||||
label={t('application.currentModel')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
fieldNames={{
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
}}
|
||||
options={modelList}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormItem>
|
||||
{source === 'chat' && <FormItem name="label" hidden />}
|
||||
|
||||
<div className="rb:text-[14px] rb:font-medium rb:text-[#5B6167] rb:mb-[16px]">{t('application.parameterConfig')}</div>
|
||||
|
||||
{configFields.map(item => (
|
||||
<FormItem
|
||||
key={item.key}
|
||||
name={item.key}
|
||||
label={t(`application.${item.key}`)}
|
||||
extra={t(`application.${item.key}_desc`)}
|
||||
>
|
||||
<RbSlider
|
||||
max={item.max}
|
||||
step={item.step}
|
||||
min={item.min}
|
||||
/>
|
||||
</FormItem>
|
||||
))}
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ModelConfigModal;
|
||||
100
web/src/views/ApplicationConfig/components/ReleaseModal.tsx
Normal file
100
web/src/views/ApplicationConfig/components/ReleaseModal.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ReleaseModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { publishRelease } from '@/api/application'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface ReleaseModalProps {
|
||||
refreshTable: () => void;
|
||||
data: Application
|
||||
}
|
||||
|
||||
const ReleaseModal = forwardRef<ReleaseModalRef, ReleaseModalProps>(({
|
||||
refreshTable,
|
||||
data
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form.validateFields().then(() => {
|
||||
setLoading(true)
|
||||
const values = form.getFieldsValue()
|
||||
publishRelease(data.id, values)
|
||||
.then(() => {
|
||||
handleClose()
|
||||
refreshTable()
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<RbModal
|
||||
title={t('application.releaseNewVersion')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('application.release')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
{/* 版本名 */}
|
||||
<FormItem
|
||||
name="version_name"
|
||||
label={t('application.versionName')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
]}
|
||||
extra={t('application.versionNameTip')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
{/* 版本描述 */}
|
||||
<FormItem
|
||||
name="release_notes"
|
||||
label={t('application.versionDescription')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
]}
|
||||
extra={t('application.versionDescriptionTip')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default ReleaseModal;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Button, App } from 'antd';
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import type { Release, ReleaseShareModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { shareRelease } from '@/api/application'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
|
||||
interface ReleaseShareModalProps {
|
||||
version: Release | null
|
||||
}
|
||||
|
||||
const ReleaseShareModal = forwardRef<ReleaseShareModalRef, ReleaseShareModalProps>(({
|
||||
version
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [shareLink, setShareLink] = useState<string | null>(null)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
if (!version) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
shareRelease(version?.app_id, version.id || '')
|
||||
.then(res => {
|
||||
const response = res as { share_token: string }
|
||||
if (response?.share_token) {
|
||||
setShareLink(`${window.location.origin}/#/conversation/${response?.share_token}`)
|
||||
setVisible(true);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
};
|
||||
const handleCopy = () => {
|
||||
if (!shareLink) return
|
||||
copy(shareLink)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={<>{t('application.shareVersion')} {version?.version}</>}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={false}
|
||||
>
|
||||
<>
|
||||
<div className="rb:leading-[20px] rb:mb-[8px]">{t('application.shareLink')}</div>
|
||||
<div className="rb:mb-[12px] rb:flex rb:items-center rb:gap-[10px] rb:justify-between">
|
||||
<div className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis rb:cursor-pointer rb:h-[32px] rb:p-[6px_10px] rb:bg-[#FFFFFF] rb:border rb:border-[#EBEBEB] rb:rounded-[6px] rb:leading-[20px]">{shareLink}</div>
|
||||
|
||||
<Button type="primary" loading={loading} disabled={!shareLink} onClick={handleCopy}>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
<RbAlert color="orange" icon={<ExclamationCircleFilled />}>
|
||||
{t('application.shareLinkTip')}
|
||||
</RbAlert>
|
||||
</>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ReleaseShareModal;
|
||||
116
web/src/views/ApplicationConfig/components/SubAgentModal.tsx
Normal file
116
web/src/views/ApplicationConfig/components/SubAgentModal.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { forwardRef, useImperativeHandle, useState, type Key } from 'react';
|
||||
import { Form, Select, Input } from 'antd';
|
||||
import type { DefaultOptionType } from 'antd/es/select'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { SubAgentModalRef, SubAgentItem } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect';
|
||||
import { getApplicationListUrl } from '@/api/application';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface SubAgentModalProps {
|
||||
refresh: (agent: SubAgentItem) => void;
|
||||
}
|
||||
|
||||
const SubAgentModal = forwardRef<SubAgentModalRef, SubAgentModalProps>(({
|
||||
refresh,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<SubAgentItem>()
|
||||
const values = Form.useWatch([], form)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = (agent?: SubAgentItem) => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue(agent)
|
||||
setEditVo(agent)
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form.validateFields().then(() => {
|
||||
setLoading(false)
|
||||
refresh(values)
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
const handleChange = (value: Key, option?: DefaultOptionType | DefaultOptionType[] | undefined) => {
|
||||
console.log(value, option)
|
||||
if (option && !Array.isArray(option)) {
|
||||
form.setFieldsValue({ name: option.children })
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t(`application.${editVo?.agent_id ? 'updateSubAgent' : 'addSubAgent'}`)}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
{/* Agent名称 */}
|
||||
<FormItem
|
||||
name="agent_id"
|
||||
label={t('application.agentName')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getApplicationListUrl}
|
||||
params={{ pagesize: 100, status: 'active', type: 'agent' }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
optionFilterProp="search"
|
||||
showSearch={true}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="name" hidden />
|
||||
{/* 描述 */}
|
||||
<FormItem
|
||||
name="role"
|
||||
label={t('application.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
{/* 关键词 */}
|
||||
<FormItem
|
||||
name="capabilities"
|
||||
label={t('application.capabilities')}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default SubAgentModal;
|
||||
23
web/src/views/ApplicationConfig/components/Tag.tsx
Normal file
23
web/src/views/ApplicationConfig/components/Tag.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
|
||||
export interface TagProps {
|
||||
color?: 'processing' | 'warning' | 'default' | 'success';
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const colors = {
|
||||
processing: 'rb:text-[#155EEF] rb:border-[rgba(21,94,239,0.25)] rb:bg-[rgba(21,94,239,0.06)]',
|
||||
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)]',
|
||||
}
|
||||
|
||||
const Tag: FC<TagProps> = ({ color = 'processing', children, className }) => {
|
||||
return (
|
||||
<span className={`rb:inline-block rb:px-[8px] rb:py-[2px] rb:rounded-[11px] rb:text-[12px] rb:font-regular rb:leading-[16px] rb:border-[1px] ${colors[color]} ${className || ''}`}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
export default Tag
|
||||
232
web/src/views/ApplicationConfig/components/VariableEditModal.tsx
Normal file
232
web/src/views/ApplicationConfig/components/VariableEditModal.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
|
||||
import { Form, Input, Select, InputNumber, Checkbox, Tag, Divider, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ApiExtensionModalRef, Variable, VariableEditModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import SortableList from '@/components/SortableList'
|
||||
import ApiExtensionModal from './ApiExtensionModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface VariableEditModalProps {
|
||||
refreshTable: (values: Variable) => void;
|
||||
}
|
||||
|
||||
const types = [
|
||||
'text',
|
||||
'paragraph',
|
||||
// 'dropdownOptions',
|
||||
'number',
|
||||
// 'checkbox',
|
||||
// 'apiVariable'
|
||||
]
|
||||
const variableType = {
|
||||
text: 'string',
|
||||
paragraph: 'string',
|
||||
dropdownOptions: 'string',
|
||||
number: 'number',
|
||||
checkbox: 'boolean',
|
||||
apiVariable: 'string',
|
||||
}
|
||||
|
||||
const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProps>(({
|
||||
refreshTable
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<Variable>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<Variable | null>(null)
|
||||
const apiExtensionModalRef = useRef<ApiExtensionModalRef>(null)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setEditVo(null)
|
||||
};
|
||||
|
||||
const handleOpen = (variable?: Variable) => {
|
||||
setVisible(true);
|
||||
if (variable) {
|
||||
setEditVo(variable || null)
|
||||
form.setFieldsValue(variable)
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form.validateFields().then((values) => {
|
||||
refreshTable({
|
||||
...(editVo || {}),
|
||||
...values,
|
||||
})
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
// 变量类型改变时,更新初始化其他字段值
|
||||
const handleChangeType = (value: Variable['type']) => {
|
||||
if (value) {
|
||||
form.setFieldsValue({
|
||||
type: value,
|
||||
name: undefined,
|
||||
display_name: undefined,
|
||||
description: undefined,
|
||||
max_length: undefined,
|
||||
options: undefined,
|
||||
api_extension: undefined,
|
||||
// default_value: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
// 添加 API 扩展
|
||||
const addApiExtension = () => {
|
||||
apiExtensionModalRef.current?.handleOpen()
|
||||
}
|
||||
const refreshApiExtensionList = () => {}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RbModal
|
||||
title={t('application.editVariable')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
{/* 变量类型 */}
|
||||
<FormItem
|
||||
name="type"
|
||||
label={t('application.variableType')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
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>}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* 变量名称 */}
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('application.variableName')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('application.invalidVariableName') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
{/* 显示名称 */}
|
||||
<FormItem
|
||||
name="display_name"
|
||||
label={t('application.displayName')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
{/* 描述 */}
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('application.description')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
{/* 最大长度 */}
|
||||
{['text', 'paragraph'].includes(values?.type) && (
|
||||
<FormItem
|
||||
name="max_length"
|
||||
label={t('application.maxLength')}
|
||||
>
|
||||
<InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />
|
||||
</FormItem>
|
||||
)}
|
||||
{/* 默认值 */}
|
||||
{/* {['text', 'paragraph', 'number', 'checkbox'].includes(values?.type) && (
|
||||
<FormItem
|
||||
name="default_value"
|
||||
label={t('application.defaultValue')}
|
||||
>
|
||||
{['text'].includes(values.type) && <Input placeholder={t('common.enter')} />}
|
||||
{['paragraph'].includes(values.type) && <Input.TextArea placeholder={t('common.enter')} />}
|
||||
{['number'].includes(values.type) && <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />}
|
||||
{['checkbox'].includes(values.type) && <Select options={[{ value: true, label: t('application.defaultChecked') }, { value: false, label: t('application.notDefaultChecked') }]} />}
|
||||
</FormItem>
|
||||
)} */}
|
||||
{/* 选项 */}
|
||||
{['dropdownOptions'].includes(values?.type) && (
|
||||
<FormItem
|
||||
name="options"
|
||||
label={t('application.options')}
|
||||
>
|
||||
<SortableList />
|
||||
</FormItem>
|
||||
)}
|
||||
{/* API 扩展 */}
|
||||
{['apiVariable'].includes(values?.type) && (
|
||||
<FormItem
|
||||
name="api_extension"
|
||||
label={t('application.apiExtension')}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Button type="text" block onClick={addApiExtension}>
|
||||
Add item
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
{/* 是否必填 */}
|
||||
<FormItem
|
||||
name="required"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>{t('application.required')}</Checkbox>
|
||||
</FormItem>
|
||||
{/* 是否隐藏 */}
|
||||
{/* <FormItem
|
||||
name="hidden"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>{t('application.hidden')}</Checkbox>
|
||||
</FormItem> */}
|
||||
</Form>
|
||||
</RbModal>
|
||||
|
||||
<ApiExtensionModal
|
||||
ref={apiExtensionModalRef}
|
||||
refresh={refreshApiExtensionList}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default VariableEditModal;
|
||||
131
web/src/views/ApplicationConfig/components/VariableList.tsx
Normal file
131
web/src/views/ApplicationConfig/components/VariableList.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { type FC, useRef, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, Switch } from 'antd'
|
||||
import variablesEmpty from '@/assets/images/application/variablesEmpty.svg'
|
||||
import Card from './Card'
|
||||
import Table from '@/components/Table';
|
||||
import type { Variable, VariableEditModalRef } from '../types'
|
||||
import Empty from '@/components/Empty'
|
||||
import VariableEditModal from './VariableEditModal'
|
||||
|
||||
interface VariableListProps {
|
||||
data?: Variable[];
|
||||
onUpdate: (data: Variable[]) => void;
|
||||
}
|
||||
const VariableList: FC<VariableListProps> = ({data = [], onUpdate}) => {
|
||||
const { t } = useTranslation()
|
||||
const variableEditModalRef = useRef<VariableEditModalRef>(null)
|
||||
const [variableList, setVariableList] = useState<Variable[]>([])
|
||||
const [maxIndex, setMaxIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || data.length === 0) return
|
||||
const list = data.map((item, index) => ({
|
||||
...item,
|
||||
index
|
||||
}))
|
||||
setVariableList(list)
|
||||
onUpdate(list)
|
||||
setMaxIndex(list.length)
|
||||
}, [data])
|
||||
|
||||
const handleAddVariable = () => {
|
||||
variableEditModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleSaveVariable = (value: Variable) => {
|
||||
if (value.index !== undefined && value.index >= 0) {
|
||||
const index = variableList.findIndex(item => item.index === value.index)
|
||||
if (index !== -1) {
|
||||
const newData = [...variableList]
|
||||
newData[index] = value
|
||||
setVariableList([...newData])
|
||||
onUpdate([...newData])
|
||||
}
|
||||
} else {
|
||||
const list = [...variableList, {
|
||||
index: maxIndex + 1,
|
||||
...value
|
||||
}]
|
||||
setVariableList(list)
|
||||
onUpdate([...list])
|
||||
setMaxIndex(maxIndex + 1)
|
||||
}
|
||||
}
|
||||
const handleDeleteVariable = (index: number) => {
|
||||
const list = variableList.filter((_, i) => i !== index)
|
||||
setVariableList(list)
|
||||
onUpdate([...list])
|
||||
}
|
||||
return (
|
||||
<Card title={t('application.variableConfiguration')}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-[11px]">
|
||||
<div className="rb:font-medium rb:leading-[20px]">
|
||||
{t('application.VariableManagement')}
|
||||
<span className="rb:font-regular rb:text-[12px] rb:text-[#5B6167]"> ({t('application.VariableManagementDesc')})</span>
|
||||
</div>
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAddVariable}>+{t('application.addVariables')}</Button>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
{variableList.length > 0
|
||||
? (
|
||||
<div className="rb:mt-[12px]">
|
||||
<Table
|
||||
rowKey="index"
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: t('application.variableType'),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type) => t(`application.${type}`)
|
||||
},
|
||||
{
|
||||
title: t('application.variableKey'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: t('application.variableName'),
|
||||
dataIndex: 'display_name',
|
||||
key: 'display_name',
|
||||
},
|
||||
{
|
||||
title: t('application.optional'),
|
||||
dataIndex: 'required',
|
||||
key: 'required',
|
||||
render: (required) => <Switch checked={!required} disabled />
|
||||
},
|
||||
{
|
||||
title: t('common.operation'),
|
||||
key: 'action',
|
||||
render: (_, record, index: number) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => variableEditModalRef.current?.handleOpen(record as Variable)}
|
||||
>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button type="link" danger onClick={() => handleDeleteVariable(index)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
initialData={variableList as unknown as Record<string, unknown>[]}
|
||||
emptySize={88}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: <Empty url={variablesEmpty} size={88} subTitle={t('application.variablesEmpty')} />
|
||||
}
|
||||
<VariableEditModal
|
||||
ref={variableEditModalRef}
|
||||
refreshTable={handleSaveVariable}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default VariableList
|
||||
Reference in New Issue
Block a user