✨ feat(models): Revamp EditModelModal UI and UX
This commit significantly refactors the `EditModelModal` component to streamline the user interface and enhance usability, aligning it with the interaction patterns found elsewhere in the application. - **Consolidated Layout:** Merged the "Vendor Information" and "Feature Configuration" sections into a single "Basic Information" card. This simplifies the form, reduces clutter, and makes all settings accessible in one view. - **Improved Prefill Groups:** Replaced the separate `Select` dropdowns for tag and endpoint groups with a more intuitive button-based system within the `extraText` of the `TagInput` components. - **Additive Button Logic:** The prefill group buttons now operate in an additive mode. Users can click multiple group buttons to incrementally add tags or endpoints, with duplicates being automatically handled. - **Clear Functionality:** Added "Clear" buttons for both tags and endpoints, allowing users to easily reset the fields. - **Code Cleanup:** Removed the unused `endpointOptions` constant and unnecessary icon imports (`Building`, `Settings`) to keep the codebase clean.
This commit is contained in:
@@ -1174,27 +1174,27 @@ const EditChannelModal = (props) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{isEdit && isMultiKeyChannel && (
|
||||
<Form.Select
|
||||
field='key_mode'
|
||||
label={t('密钥更新模式')}
|
||||
placeholder={t('请选择密钥更新模式')}
|
||||
optionList={[
|
||||
{ label: t('追加到现有密钥'), value: 'append' },
|
||||
{ label: t('覆盖现有密钥'), value: 'replace' },
|
||||
]}
|
||||
style={{ width: '100%' }}
|
||||
value={keyMode}
|
||||
onChange={(value) => setKeyMode(value)}
|
||||
extraText={
|
||||
<Text type="tertiary" size="small">
|
||||
{keyMode === 'replace'
|
||||
? t('覆盖模式:将完全替换现有的所有密钥')
|
||||
: t('追加模式:将新密钥添加到现有密钥列表末尾')
|
||||
}
|
||||
</Text>
|
||||
{isEdit && isMultiKeyChannel && (
|
||||
<Form.Select
|
||||
field='key_mode'
|
||||
label={t('密钥更新模式')}
|
||||
placeholder={t('请选择密钥更新模式')}
|
||||
optionList={[
|
||||
{ label: t('追加到现有密钥'), value: 'append' },
|
||||
{ label: t('覆盖现有密钥'), value: 'replace' },
|
||||
]}
|
||||
style={{ width: '100%' }}
|
||||
value={keyMode}
|
||||
onChange={(value) => setKeyMode(value)}
|
||||
extraText={
|
||||
<Text type="tertiary" size="small">
|
||||
{keyMode === 'replace'
|
||||
? t('覆盖模式:将完全替换现有的所有密钥')
|
||||
: t('追加模式:将新密钥添加到现有密钥列表末尾')
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{batch && multiToSingle && (
|
||||
<>
|
||||
|
||||
@@ -175,7 +175,7 @@ const ModelTestModal = ({
|
||||
<Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
|
||||
{currentTestChannel.name} {t('渠道的模型测试')}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="tertiary" className="!text-xs flex items-center">
|
||||
<Typography.Text type="tertiary" size="small">
|
||||
{t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -20,13 +20,15 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import React, { useState } from 'react';
|
||||
import MissingModelsModal from './modals/MissingModelsModal.jsx';
|
||||
import PrefillGroupManagement from './modals/PrefillGroupManagement.jsx';
|
||||
import { Button, Space, Modal } from '@douyinfe/semi-ui';
|
||||
import EditPrefillGroupModal from './modals/EditPrefillGroupModal.jsx';
|
||||
import { Button, Modal } from '@douyinfe/semi-ui';
|
||||
import { showSuccess, showError, copy } from '../../../helpers';
|
||||
import CompactModeToggle from '../../common/ui/CompactModeToggle';
|
||||
import { showError } from '../../../helpers';
|
||||
import SelectionNotification from './components/SelectionNotification.jsx';
|
||||
|
||||
const ModelsActions = ({
|
||||
selectedKeys,
|
||||
setSelectedKeys,
|
||||
setEditingModel,
|
||||
setShowEdit,
|
||||
batchDeleteModels,
|
||||
@@ -38,13 +40,11 @@ const ModelsActions = ({
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showMissingModal, setShowMissingModal] = useState(false);
|
||||
const [showGroupManagement, setShowGroupManagement] = useState(false);
|
||||
const [showAddPrefill, setShowAddPrefill] = useState(false);
|
||||
const [prefillInit, setPrefillInit] = useState({ id: undefined });
|
||||
|
||||
// Handle delete selected models with confirmation
|
||||
const handleDeleteSelectedModels = () => {
|
||||
if (selectedKeys.length === 0) {
|
||||
showError(t('请至少选择一个模型!'));
|
||||
return;
|
||||
}
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
@@ -54,6 +54,30 @@ const ModelsActions = ({
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
// Handle clear selection
|
||||
const handleClearSelected = () => {
|
||||
setSelectedKeys([]);
|
||||
};
|
||||
|
||||
// Handle add selected models to prefill group
|
||||
const handleCopyNames = async () => {
|
||||
const text = selectedKeys.map(m => m.model_name).join(',');
|
||||
if (!text) return;
|
||||
const ok = await copy(text);
|
||||
if (ok) {
|
||||
showSuccess(t('已复制模型名称'));
|
||||
} else {
|
||||
showError(t('复制失败'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddToPrefill = () => {
|
||||
// Prepare initial data
|
||||
const items = selectedKeys.map((m) => m.model_name);
|
||||
setPrefillInit({ id: undefined, type: 'model', items });
|
||||
setShowAddPrefill(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1">
|
||||
@@ -71,7 +95,6 @@ const ModelsActions = ({
|
||||
{t('添加模型')}
|
||||
</Button>
|
||||
|
||||
|
||||
<Button
|
||||
type="secondary"
|
||||
className="flex-1 md:flex-initial"
|
||||
@@ -101,6 +124,9 @@ const ModelsActions = ({
|
||||
selectedKeys={selectedKeys}
|
||||
t={t}
|
||||
onDelete={handleDeleteSelectedModels}
|
||||
onAddPrefill={handleAddToPrefill}
|
||||
onClear={handleClearSelected}
|
||||
onCopy={handleCopyNames}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
@@ -130,6 +156,13 @@ const ModelsActions = ({
|
||||
visible={showGroupManagement}
|
||||
onClose={() => setShowGroupManagement(false)}
|
||||
/>
|
||||
|
||||
<EditPrefillGroupModal
|
||||
visible={showAddPrefill}
|
||||
onClose={() => setShowAddPrefill(false)}
|
||||
editingGroup={prefillInit}
|
||||
onSuccess={() => setShowAddPrefill(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { Notification, Button, Space } from '@douyinfe/semi-ui';
|
||||
import { Notification, Button, Space, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
// 固定通知 ID,保持同一个实例即可避免闪烁
|
||||
const NOTICE_ID = 'models-batch-actions';
|
||||
@@ -28,22 +28,52 @@ const NOTICE_ID = 'models-batch-actions';
|
||||
* 1. 当 selectedKeys.length > 0 时,使用固定 id 创建/更新通知
|
||||
* 2. 当 selectedKeys 清空时关闭通知
|
||||
*/
|
||||
const SelectionNotification = ({ selectedKeys = [], t, onDelete }) => {
|
||||
const SelectionNotification = ({ selectedKeys = [], t, onDelete, onAddPrefill, onClear, onCopy }) => {
|
||||
// 根据选中数量决定显示/隐藏或更新通知
|
||||
useEffect(() => {
|
||||
const selectedCount = selectedKeys.length;
|
||||
|
||||
if (selectedCount > 0) {
|
||||
const titleNode = (
|
||||
<Space wrap>
|
||||
<span>{t('批量操作')}</span>
|
||||
<Typography.Text type="tertiary" size="small">{t('已选择 {{count}} 个模型', { count: selectedCount })}</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<Space>
|
||||
<span>{t('已选择 {{count}} 个模型', { count: selectedCount })}</span>
|
||||
<Space wrap>
|
||||
<Button
|
||||
size="small"
|
||||
type="secondary"
|
||||
theme="solid"
|
||||
onClick={onClear}
|
||||
>
|
||||
{t('取消全选')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
theme="solid"
|
||||
onClick={onAddPrefill}
|
||||
>
|
||||
{t('加入预填组')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="tertiary"
|
||||
theme="solid"
|
||||
onClick={onCopy}
|
||||
>
|
||||
{t('复制名称')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="danger"
|
||||
theme="solid"
|
||||
onClick={onDelete}
|
||||
>
|
||||
{t('删除所选模型')}
|
||||
{t('删除所选')}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
@@ -51,7 +81,7 @@ const SelectionNotification = ({ selectedKeys = [], t, onDelete }) => {
|
||||
// 使用相同 id 更新通知(若已存在则就地更新,不存在则创建)
|
||||
Notification.info({
|
||||
id: NOTICE_ID,
|
||||
title: t('批量操作'),
|
||||
title: titleNode,
|
||||
content,
|
||||
duration: 0, // 不自动关闭
|
||||
position: 'bottom',
|
||||
@@ -61,7 +91,7 @@ const SelectionNotification = ({ selectedKeys = [], t, onDelete }) => {
|
||||
// 取消全部勾选时关闭通知
|
||||
Notification.close(NOTICE_ID);
|
||||
}
|
||||
}, [selectedKeys, t, onDelete]);
|
||||
}, [selectedKeys, t, onDelete, onAddPrefill, onClear, onCopy]);
|
||||
|
||||
// 卸载时确保关闭通知
|
||||
useEffect(() => {
|
||||
|
||||
@@ -42,6 +42,7 @@ const ModelsPage = () => {
|
||||
|
||||
// Actions state
|
||||
selectedKeys,
|
||||
setSelectedKeys,
|
||||
setEditingModel,
|
||||
setShowEdit,
|
||||
batchDeleteModels,
|
||||
@@ -100,6 +101,7 @@ const ModelsPage = () => {
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-2 w-full">
|
||||
<ModelsActions
|
||||
selectedKeys={selectedKeys}
|
||||
setSelectedKeys={setSelectedKeys}
|
||||
setEditingModel={setEditingModel}
|
||||
setShowEdit={setShowEdit}
|
||||
batchDeleteModels={batchDeleteModels}
|
||||
|
||||
@@ -35,13 +35,13 @@ import {
|
||||
Save,
|
||||
X,
|
||||
FileText,
|
||||
Building,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
import { API, showError, showSuccess } from '../../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const nameRuleOptions = [
|
||||
{ label: '精确名称匹配', value: 0 },
|
||||
{ label: '前缀名称匹配', value: 1 },
|
||||
@@ -49,16 +49,6 @@ const nameRuleOptions = [
|
||||
{ label: '后缀名称匹配', value: 3 },
|
||||
];
|
||||
|
||||
const endpointOptions = [
|
||||
{ label: 'OpenAI', value: 'openai' },
|
||||
{ label: 'Anthropic', value: 'anthropic' },
|
||||
{ label: 'Gemini', value: 'gemini' },
|
||||
{ label: 'Image Generation', value: 'image-generation' },
|
||||
{ label: 'Jina Rerank', value: 'jina-rerank' },
|
||||
];
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const EditModelModal = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -332,23 +322,6 @@ const EditModelModal = (props) => {
|
||||
showClear
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Select
|
||||
field='tag_group'
|
||||
label={t('标签组')}
|
||||
placeholder={t('选择标签组后将自动填充标签')}
|
||||
optionList={tagGroups.map(g => ({ label: g.name, value: g.id }))}
|
||||
showClear
|
||||
onChange={(value) => {
|
||||
const g = tagGroups.find(item => item.id === value);
|
||||
if (g && formApiRef.current) {
|
||||
formApiRef.current.setValue('tags', g.items || []);
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.TagInput
|
||||
field='tags'
|
||||
@@ -366,23 +339,40 @@ const EditModelModal = (props) => {
|
||||
formApiRef.current.setValue('tags', normalized);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
extraText={(
|
||||
<Space wrap>
|
||||
{tagGroups.map(group => (
|
||||
<Button
|
||||
key={group.id}
|
||||
size='small'
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
if (formApiRef.current) {
|
||||
const currentTags = formApiRef.current.getValue('tags') || [];
|
||||
const newTags = [...currentTags, ...(group.items || [])];
|
||||
const uniqueTags = [...new Set(newTags)];
|
||||
formApiRef.current.setValue('tags', uniqueTags);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{group.name}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
size='small'
|
||||
type='warning'
|
||||
onClick={() => {
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('tags', []);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('清除标签')}
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 供应商信息 */}
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
<div className='flex items-center mb-2'>
|
||||
<Avatar size='small' color='blue' className='mr-2 shadow-md'>
|
||||
<Building size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Text className='text-lg font-medium'>{t('供应商信息')}</Text>
|
||||
<div className='text-xs text-gray-600'>{t('设置模型的供应商相关信息')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Form.Select
|
||||
field='vendor_id'
|
||||
@@ -400,47 +390,46 @@ const EditModelModal = (props) => {
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 功能配置 */}
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
<div className='flex items-center mb-2'>
|
||||
<Avatar size='small' color='purple' className='mr-2 shadow-md'>
|
||||
<Settings size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Text className='text-lg font-medium'>{t('功能配置')}</Text>
|
||||
<div className='text-xs text-gray-600'>{t('设置模型的功能和状态')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Form.Select
|
||||
field='endpoint_group'
|
||||
label={t('端点组')}
|
||||
placeholder={t('选择端点组后将自动填充端点')}
|
||||
optionList={endpointGroups.map(g => ({ label: g.name, value: g.id }))}
|
||||
showClear
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value) => {
|
||||
const g = endpointGroups.find(item => item.id === value);
|
||||
if (g && formApiRef.current) {
|
||||
formApiRef.current.setValue('endpoints', g.items || []);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Select
|
||||
<Form.TagInput
|
||||
field='endpoints'
|
||||
label={t('支持端点')}
|
||||
placeholder={t('选择模型支持的端点类型')}
|
||||
optionList={endpointOptions}
|
||||
multiple
|
||||
placeholder={t('输入端点名称,按回车添加')}
|
||||
addOnBlur
|
||||
showClear
|
||||
style={{ width: '100%' }}
|
||||
extraText={(
|
||||
<Space wrap>
|
||||
{endpointGroups.map(group => (
|
||||
<Button
|
||||
key={group.id}
|
||||
size='small'
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
if (formApiRef.current) {
|
||||
const currentEndpoints = formApiRef.current.getValue('endpoints') || [];
|
||||
const newEndpoints = [...currentEndpoints, ...(group.items || [])];
|
||||
const uniqueEndpoints = [...new Set(newEndpoints)];
|
||||
formApiRef.current.setValue('endpoints', uniqueEndpoints);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{group.name}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
size='small'
|
||||
type='warning'
|
||||
onClick={() => {
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('endpoints', []);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('清除端点')}
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
|
||||
@@ -111,7 +111,7 @@ const MissingModelsModal = ({
|
||||
<Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
|
||||
{t('未配置的模型列表')}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="tertiary" className="!text-xs flex items-center">
|
||||
<Typography.Text type="tertiary" size="small">
|
||||
{t('共')} {missingModels.length} {t('个未配置模型')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -328,6 +328,7 @@ export const useModelsData = () => {
|
||||
selectedKeys,
|
||||
rowSelection,
|
||||
handleRow,
|
||||
setSelectedKeys,
|
||||
|
||||
// Modal state
|
||||
showEdit,
|
||||
|
||||
Reference in New Issue
Block a user