🎨 chore(web): apply ESLint and Prettier auto-fixes (baseline)

- Ran: bun run eslint:fix && bun run lint:fix
- Inserted AGPL license header via eslint-plugin-header
- Enforced no-multiple-empty-lines and other lint rules
- Formatted code using Prettier v3 (@so1ve/prettier-config)
- No functional changes; formatting-only baseline across JS/JSX files
This commit is contained in:
t0ng7u
2025-08-30 21:15:10 +08:00
parent 41cf516ec5
commit 0d57b1acd4
274 changed files with 11025 additions and 7659 deletions

View File

@@ -61,7 +61,7 @@ const ModelsActions = ({
// Handle add selected models to prefill group
const handleCopyNames = async () => {
const text = selectedKeys.map(m => m.model_name).join(',');
const text = selectedKeys.map((m) => m.model_name).join(',');
if (!text) return;
const ok = await copy(text);
if (ok) {
@@ -80,34 +80,34 @@ const ModelsActions = ({
return (
<>
<div className="flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1">
<div className='flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1'>
<Button
type="primary"
className="flex-1 md:flex-initial"
type='primary'
className='flex-1 md:flex-initial'
onClick={() => {
setEditingModel({
id: undefined,
});
setShowEdit(true);
}}
size="small"
size='small'
>
{t('添加模型')}
</Button>
<Button
type="secondary"
className="flex-1 md:flex-initial"
size="small"
type='secondary'
className='flex-1 md:flex-initial'
size='small'
onClick={() => setShowMissingModal(true)}
>
{t('未配置模型')}
</Button>
<Button
type="secondary"
className="flex-1 md:flex-initial"
size="small"
type='secondary'
className='flex-1 md:flex-initial'
size='small'
onClick={() => setShowGroupManagement(true)}
>
{t('预填组管理')}
@@ -134,10 +134,12 @@ const ModelsActions = ({
visible={showDeleteModal}
onCancel={() => setShowDeleteModal(false)}
onOk={handleConfirmDelete}
type="warning"
type='warning'
>
<div>
{t('确定要删除所选的 {{count}} 个模型吗?', { count: selectedKeys.length })}
{t('确定要删除所选的 {{count}} 个模型吗?', {
count: selectedKeys.length,
})}
</div>
</Modal>
@@ -167,4 +169,4 @@ const ModelsActions = ({
);
};
export default ModelsActions;
export default ModelsActions;

View File

@@ -18,13 +18,23 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Button, Space, Tag, Typography, Modal, Tooltip } from '@douyinfe/semi-ui';
import {
Button,
Space,
Tag,
Typography,
Modal,
Tooltip,
} from '@douyinfe/semi-ui';
import {
timestamp2string,
getLobeHubIcon,
stringToColor
stringToColor,
} from '../../../helpers';
import { renderLimitedItems, renderDescription } from '../../common/ui/RenderUtils';
import {
renderLimitedItems,
renderDescription,
} from '../../common/ui/RenderUtils';
const { Text } = Typography;
@@ -38,7 +48,7 @@ const renderModelIconCol = (record, vendorMap) => {
const iconKey = record?.icon || vendorMap[record?.vendor_id]?.icon;
if (!iconKey) return '-';
return (
<div className="flex items-center justify-center">
<div className='flex items-center justify-center'>
{getLobeHubIcon(iconKey, 20)}
</div>
);
@@ -65,7 +75,7 @@ const renderGroups = (groups) => {
return renderLimitedItems({
items: groups,
renderItem: (g, idx) => (
<Tag key={idx} size="small" shape='circle' color={stringToColor(g)}>
<Tag key={idx} size='small' shape='circle' color={stringToColor(g)}>
{g}
</Tag>
),
@@ -79,7 +89,7 @@ const renderTags = (text) => {
return renderLimitedItems({
items: tagsArr,
renderItem: (tag, idx) => (
<Tag key={idx} size="small" shape='circle' color={stringToColor(tag)}>
<Tag key={idx} size='small' shape='circle' color={stringToColor(tag)}>
{tag}
</Tag>
),
@@ -96,7 +106,7 @@ const renderEndpoints = (value) => {
return renderLimitedItems({
items: keys,
renderItem: (key, idx) => (
<Tag key={idx} size="small" shape='circle' color={stringToColor(key)}>
<Tag key={idx} size='small' shape='circle' color={stringToColor(key)}>
{key}
</Tag>
),
@@ -108,7 +118,7 @@ const renderEndpoints = (value) => {
return renderLimitedItems({
items: parsed,
renderItem: (ep, idx) => (
<Tag key={idx} color="white" size="small" shape='circle'>
<Tag key={idx} color='white' size='small' shape='circle'>
{ep}
</Tag>
),
@@ -157,7 +167,7 @@ const renderBoundChannels = (channels) => {
return renderLimitedItems({
items: channels,
renderItem: (c, idx) => (
<Tag key={idx} color="white" size="small" shape='circle'>
<Tag key={idx} color='white' size='small' shape='circle'>
{c.name}({c.type})
</Tag>
),
@@ -165,20 +175,28 @@ const renderBoundChannels = (channels) => {
};
// Render operations column
const renderOperations = (text, record, setEditingModel, setShowEdit, manageModel, refresh, t) => {
const renderOperations = (
text,
record,
setEditingModel,
setShowEdit,
manageModel,
refresh,
t,
) => {
return (
<Space wrap>
{record.status === 1 ? (
<Button
type='danger'
size="small"
size='small'
onClick={() => manageModel(record.id, 'disable', record)}
>
{t('禁用')}
</Button>
) : (
<Button
size="small"
size='small'
onClick={() => manageModel(record.id, 'enable', record)}
>
{t('启用')}
@@ -187,7 +205,7 @@ const renderOperations = (text, record, setEditingModel, setShowEdit, manageMode
<Button
type='tertiary'
size="small"
size='small'
onClick={() => {
setEditingModel(record);
setShowEdit(true);
@@ -198,7 +216,7 @@ const renderOperations = (text, record, setEditingModel, setShowEdit, manageMode
<Button
type='danger'
size="small"
size='small'
onClick={() => {
Modal.confirm({
title: t('确定是否要删除此模型?'),
@@ -235,12 +253,16 @@ const renderNameRule = (rule, record, t) => {
}
const tagElement = (
<Tag color={cfg.color} size="small" shape='circle'>
<Tag color={cfg.color} size='small' shape='circle'>
{label}
</Tag>
);
if (rule === 0 || !record.matched_models || record.matched_models.length === 0) {
if (
rule === 0 ||
!record.matched_models ||
record.matched_models.length === 0
) {
return tagElement;
}
@@ -334,15 +356,16 @@ export const getModelsColumns = ({
title: '',
dataIndex: 'operate',
fixed: 'right',
render: (text, record, index) => renderOperations(
text,
record,
setEditingModel,
setShowEdit,
manageModel,
refresh,
t
),
render: (text, record, index) =>
renderOperations(
text,
record,
setEditingModel,
setShowEdit,
manageModel,
refresh,
t,
),
},
];
};
};

View File

@@ -26,9 +26,9 @@ const { Text } = Typography;
const ModelsDescription = ({ compactMode, setCompactMode, t }) => {
return (
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
<div className="flex items-center text-green-500">
<Layers size={16} className="mr-2" />
<div className='flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full'>
<div className='flex items-center text-green-500'>
<Layers size={16} className='mr-2' />
<Text>{t('模型管理')}</Text>
</div>
@@ -41,4 +41,4 @@ const ModelsDescription = ({ compactMode, setCompactMode, t }) => {
);
};
export default ModelsDescription;
export default ModelsDescription;

View File

@@ -49,42 +49,42 @@ const ModelsFilters = ({
}}
onSubmit={searchModels}
allowEmpty={true}
autoComplete="off"
layout="horizontal"
trigger="change"
autoComplete='off'
layout='horizontal'
trigger='change'
stopValidateWithError={false}
className="w-full md:w-auto order-1 md:order-2"
className='w-full md:w-auto order-1 md:order-2'
>
<div className="flex flex-col md:flex-row items-center gap-2 w-full md:w-auto">
<div className="relative w-full md:w-56">
<div className='flex flex-col md:flex-row items-center gap-2 w-full md:w-auto'>
<div className='relative w-full md:w-56'>
<Form.Input
field="searchKeyword"
field='searchKeyword'
prefix={<IconSearch />}
placeholder={t('搜索模型名称')}
showClear
pure
size="small"
size='small'
/>
</div>
<div className="relative w-full md:w-56">
<div className='relative w-full md:w-56'>
<Form.Input
field="searchVendor"
field='searchVendor'
prefix={<IconSearch />}
placeholder={t('搜索供应商')}
showClear
pure
size="small"
size='small'
/>
</div>
<div className="flex gap-2 w-full md:w-auto">
<div className='flex gap-2 w-full md:w-auto'>
<Button
type="tertiary"
htmlType="submit"
type='tertiary'
htmlType='submit'
loading={loading || searching}
className="flex-1 md:flex-initial md:w-auto"
size="small"
className='flex-1 md:flex-initial md:w-auto'
size='small'
>
{t('查询')}
</Button>
@@ -92,8 +92,8 @@ const ModelsFilters = ({
<Button
type='tertiary'
onClick={handleReset}
className="flex-1 md:flex-initial md:w-auto"
size="small"
className='flex-1 md:flex-initial md:w-auto'
size='small'
>
{t('重置')}
</Button>
@@ -103,4 +103,4 @@ const ModelsFilters = ({
);
};
export default ModelsFilters;
export default ModelsFilters;

View File

@@ -60,13 +60,15 @@ const ModelsTable = (modelsData) => {
// Handle compact mode by removing fixed positioning
const tableColumns = useMemo(() => {
return compactMode ? columns.map(col => {
if (col.dataIndex === 'operate') {
const { fixed, ...rest } = col;
return rest;
}
return col;
}) : columns;
return compactMode
? columns.map((col) => {
if (col.dataIndex === 'operate') {
const { fixed, ...rest } = col;
return rest;
}
return col;
})
: columns;
}, [compactMode, columns]);
return (
@@ -90,15 +92,17 @@ const ModelsTable = (modelsData) => {
empty={
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
darkModeImage={
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
}
description={t('搜索无结果')}
style={{ padding: 30 }}
/>
}
className="rounded-xl overflow-hidden"
size="middle"
className='rounded-xl overflow-hidden'
size='middle'
/>
);
};
export default ModelsTable;
export default ModelsTable;

View File

@@ -36,7 +36,7 @@ const ModelsTabs = ({
setShowEditVendor,
setEditingVendor,
loadVendors,
t
t,
}) => {
const handleTabChange = (key) => {
setActiveVendorKey(key);
@@ -75,14 +75,14 @@ const ModelsTabs = ({
return (
<Tabs
activeKey={activeVendorKey}
type="card"
type='card'
collapsible
onChange={handleTabChange}
className="mb-2"
className='mb-2'
tabBarExtraContent={
<Button
type="primary"
size="small"
type='primary'
size='small'
onClick={() => setShowAddVendor(true)}
>
{t('新增供应商')}
@@ -90,11 +90,14 @@ const ModelsTabs = ({
}
>
<TabPane
itemKey="all"
itemKey='all'
tab={
<span className="flex items-center gap-2">
<span className='flex items-center gap-2'>
{t('全部')}
<Tag color={activeVendorKey === 'all' ? 'red' : 'grey'} shape='circle'>
<Tag
color={activeVendorKey === 'all' ? 'red' : 'grey'}
shape='circle'
>
{vendorCounts['all'] || 0}
</Tag>
</span>
@@ -109,15 +112,18 @@ const ModelsTabs = ({
key={key}
itemKey={key}
tab={
<span className="flex items-center gap-2">
<span className='flex items-center gap-2'>
{getLobeHubIcon(vendor.icon || 'Layers', 14)}
{vendor.name}
<Tag color={activeVendorKey === key ? 'red' : 'grey'} shape='circle'>
<Tag
color={activeVendorKey === key ? 'red' : 'grey'}
shape='circle'
>
{count}
</Tag>
<Dropdown
trigger="click"
position="bottomRight"
trigger='click'
position='bottomRight'
render={
<Dropdown.Menu>
<Dropdown.Item
@@ -127,13 +133,16 @@ const ModelsTabs = ({
{t('编辑')}
</Dropdown.Item>
<Dropdown.Item
type="danger"
type='danger'
icon={<IconDelete />}
onClick={(e) => {
e.stopPropagation();
Modal.confirm({
title: t('确认删除'),
content: t('确定要删除供应商 "{{name}}" 吗?此操作不可撤销。', { name: vendor.name }),
content: t(
'确定要删除供应商 "{{name}}" 吗?此操作不可撤销。',
{ name: vendor.name },
),
onOk: () => handleDeleteVendor(vendor, e),
okText: t('删除'),
cancelText: t('取消'),
@@ -149,9 +158,9 @@ const ModelsTabs = ({
onClickOutSide={(e) => e.stopPropagation()}
>
<Button
size="small"
type="tertiary"
theme="outline"
size='small'
type='tertiary'
theme='outline'
onClick={(e) => e.stopPropagation()}
>
{t('操作')}
@@ -166,4 +175,4 @@ const ModelsTabs = ({
);
};
export default ModelsTabs;
export default ModelsTabs;

View File

@@ -28,7 +28,14 @@ const NOTICE_ID = 'models-batch-actions';
* 1. 当 selectedKeys.length > 0 时,使用固定 id 创建/更新通知
* 2. 当 selectedKeys 清空时关闭通知
*/
const SelectionNotification = ({ selectedKeys = [], t, onDelete, onAddPrefill, onClear, onCopy }) => {
const SelectionNotification = ({
selectedKeys = [],
t,
onDelete,
onAddPrefill,
onClear,
onCopy,
}) => {
// 根据选中数量决定显示/隐藏或更新通知
useEffect(() => {
const selectedCount = selectedKeys.length;
@@ -37,42 +44,29 @@ const SelectionNotification = ({ selectedKeys = [], t, onDelete, onAddPrefill, o
const titleNode = (
<Space wrap>
<span>{t('批量操作')}</span>
<Typography.Text type="tertiary" size="small">{t('已选择 {{count}} 个模型', { count: selectedCount })}</Typography.Text>
<Typography.Text type='tertiary' size='small'>
{t('已选择 {{count}} 个模型', { count: selectedCount })}
</Typography.Text>
</Space>
);
const content = (
<Space wrap>
<Button
size="small"
type="tertiary"
theme="solid"
onClick={onClear}
>
<Button size='small' type='tertiary' theme='solid' onClick={onClear}>
{t('取消全选')}
</Button>
<Button
size="small"
type="primary"
theme="solid"
size='small'
type='primary'
theme='solid'
onClick={onAddPrefill}
>
{t('加入预填组')}
</Button>
<Button
size="small"
type="secondary"
theme="solid"
onClick={onCopy}
>
<Button size='small' type='secondary' theme='solid' onClick={onCopy}>
{t('复制名称')}
</Button>
<Button
size="small"
type="danger"
theme="solid"
onClick={onDelete}
>
<Button size='small' type='danger' theme='solid' onClick={onDelete}>
{t('删除所选')}
</Button>
</Space>

View File

@@ -95,10 +95,10 @@ const ModelsPage = () => {
/>
<CardPro
type="type3"
type='type3'
tabsArea={<ModelsTabs {...modelsData} />}
actionsArea={
<div className="flex flex-col md:flex-row justify-between items-center gap-2 w-full">
<div className='flex flex-col md:flex-row justify-between items-center gap-2 w-full'>
<ModelsActions
selectedKeys={selectedKeys}
setSelectedKeys={setSelectedKeys}
@@ -110,7 +110,7 @@ const ModelsPage = () => {
t={t}
/>
<div className="w-full md:w-full lg:w-auto order-1 md:order-2">
<div className='w-full md:w-full lg:w-auto order-1 md:order-2'>
<ModelsFilters
formInitValues={formInitValues}
setFormApi={setFormApi}

View File

@@ -290,7 +290,9 @@ const EditModelModal = (props) => {
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('基本信息')}</Text>
<div className='text-xs text-gray-600'>{t('设置模型的基本信息')}</div>
<div className='text-xs text-gray-600'>
{t('设置模型的基本信息')}
</div>
</div>
</div>
<Row gutter={12}>
@@ -309,9 +311,16 @@ const EditModelModal = (props) => {
field='name_rule'
label={t('名称匹配类型')}
placeholder={t('请选择名称匹配类型')}
optionList={nameRuleOptions.map(o => ({ label: t(o.label), value: o.value }))}
rules={[{ required: true, message: t('请选择名称匹配类型') }]}
extraText={t('根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含')}
optionList={nameRuleOptions.map((o) => ({
label: t(o.label),
value: o.value,
}))}
rules={[
{ required: true, message: t('请选择名称匹配类型') },
]}
extraText={t(
'根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含',
)}
style={{ width: '100%' }}
/>
</Col>
@@ -323,9 +332,14 @@ const EditModelModal = (props) => {
placeholder={t('请输入图标名称')}
extraText={
<span>
{t('图标使用@lobehub/icons库OpenAI、Claude.Color支持链式参数OpenAI.Avatar.type={\'platform\'}、OpenRouter.Avatar.shape={\'square\'},查询所有可用图标请 ')}
{t(
"图标使用@lobehub/icons库OpenAI、Claude.Color支持链式参数OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ",
)}
<Typography.Text
link={{ href: 'https://icons.lobehub.com/components/lobe-hub', target: '_blank' }}
link={{
href: 'https://icons.lobehub.com/components/lobe-hub',
target: '_blank',
}}
icon={<IconLink />}
underline
>
@@ -357,7 +371,16 @@ const EditModelModal = (props) => {
if (!formApiRef.current) return;
const normalize = (tags) => {
if (!Array.isArray(tags)) return [];
return [...new Set(tags.flatMap(tag => tag.split(',').map(t => t.trim()).filter(Boolean)))];
return [
...new Set(
tags.flatMap((tag) =>
tag
.split(',')
.map((t) => t.trim())
.filter(Boolean),
),
),
];
};
const normalized = normalize(newTags);
formApiRef.current.setValue('tags', normalized);
@@ -366,17 +389,24 @@ const EditModelModal = (props) => {
{...(tagGroups.length > 0 && {
extraText: (
<Space wrap>
{tagGroups.map(group => (
{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 currentTags =
formApiRef.current.getValue('tags') || [];
const newTags = [
...currentTags,
...(group.items || []),
];
const uniqueTags = [...new Set(newTags)];
formApiRef.current.setValue('tags', uniqueTags);
formApiRef.current.setValue(
'tags',
uniqueTags,
);
}
}}
>
@@ -384,7 +414,7 @@ const EditModelModal = (props) => {
</Button>
))}
</Space>
)
),
})}
/>
</Col>
@@ -393,13 +423,19 @@ const EditModelModal = (props) => {
field='vendor_id'
label={t('供应商')}
placeholder={t('选择模型供应商')}
optionList={vendors.map(v => ({ label: v.name, value: v.id }))}
optionList={vendors.map((v) => ({
label: v.name,
value: v.id,
}))}
filter
showClear
onChange={(value) => {
const vendorInfo = vendors.find(v => v.id === value);
const vendorInfo = vendors.find((v) => v.id === value);
if (vendorInfo && formApiRef.current) {
formApiRef.current.setValue('vendor', vendorInfo.name);
formApiRef.current.setValue(
'vendor',
vendorInfo.name,
);
}
}}
style={{ width: '100%' }}
@@ -409,49 +445,71 @@ const EditModelModal = (props) => {
<JSONEditor
field='endpoints'
label={t('端点映射')}
placeholder={'{\n "openai": {"path": "/v1/chat/completions", "method": "POST"}\n}'}
placeholder={
'{\n "openai": {"path": "/v1/chat/completions", "method": "POST"}\n}'
}
value={values.endpoints}
onChange={(val) => formApiRef.current?.setValue('endpoints', val)}
onChange={(val) =>
formApiRef.current?.setValue('endpoints', val)
}
formApi={formApiRef.current}
editorType='object'
template={ENDPOINT_TEMPLATE}
templateLabel={t('填入模板')}
extraText={t('留空则使用默认端点;支持 {path, method}')}
extraFooter={endpointGroups.length > 0 && (
<Space wrap>
{endpointGroups.map(group => (
<Button
key={group.id}
size='small'
type='primary'
onClick={() => {
try {
const current = formApiRef.current?.getValue('endpoints') || '';
let base = {};
if (current && current.trim()) base = JSON.parse(current);
const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {});
const merged = { ...base, ...groupObj };
formApiRef.current?.setValue('endpoints', JSON.stringify(merged, null, 2));
} catch (e) {
extraFooter={
endpointGroups.length > 0 && (
<Space wrap>
{endpointGroups.map((group) => (
<Button
key={group.id}
size='small'
type='primary'
onClick={() => {
try {
const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {});
formApiRef.current?.setValue('endpoints', JSON.stringify(groupObj, null, 2));
} catch { }
}
}}
>
{group.name}
</Button>
))}
</Space>
)}
const current =
formApiRef.current?.getValue(
'endpoints',
) || '';
let base = {};
if (current && current.trim())
base = JSON.parse(current);
const groupObj =
typeof group.items === 'string'
? JSON.parse(group.items || '{}')
: group.items || {};
const merged = { ...base, ...groupObj };
formApiRef.current?.setValue(
'endpoints',
JSON.stringify(merged, null, 2),
);
} catch (e) {
try {
const groupObj =
typeof group.items === 'string'
? JSON.parse(group.items || '{}')
: group.items || {};
formApiRef.current?.setValue(
'endpoints',
JSON.stringify(groupObj, null, 2),
);
} catch {}
}
}}
>
{group.name}
</Button>
))}
</Space>
)
}
/>
</Col>
<Col span={24}>
<Form.Switch
field='status'
label={t('状态')}
size="large"
size='large'
/>
</Col>
</Row>
@@ -464,4 +522,4 @@ const EditModelModal = (props) => {
);
};
export default EditModelModal;
export default EditModelModal;

View File

@@ -32,11 +32,7 @@ import {
Avatar,
Spin,
} from '@douyinfe/semi-ui';
import {
IconLayers,
IconSave,
IconClose,
} from '@douyinfe/semi-icons';
import { IconLayers, IconSave, IconClose } from '@douyinfe/semi-icons';
import { API, showError, showSuccess } from '../../../../helpers';
import { useTranslation } from 'react-i18next';
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
@@ -53,7 +49,12 @@ const ENDPOINT_TEMPLATE = {
'image-generation': { path: '/v1/images/generations', method: 'POST' },
};
const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) => {
const EditPrefillGroupModal = ({
visible,
onClose,
editingGroup,
onSuccess,
}) => {
const { t } = useTranslation();
const isMobile = useIsMobile();
const [loading, setLoading] = useState(false);
@@ -112,7 +113,7 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
return (
<SideSheet
placement="left"
placement='left'
title={
<Space>
{isEdit ? (
@@ -193,13 +194,15 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('基本信息')}</Text>
<div className='text-xs text-gray-600'>{t('设置预填组的基本信息')}</div>
<div className='text-xs text-gray-600'>
{t('设置预填组的基本信息')}
</div>
</div>
</div>
<Row gutter={12}>
<Col span={24}>
<Form.Input
field="name"
field='name'
label={t('组名')}
placeholder={t('请输入组名')}
rules={[{ required: true, message: t('请输入组名') }]}
@@ -208,7 +211,7 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
</Col>
<Col span={24}>
<Form.Select
field="type"
field='type'
label={t('类型')}
placeholder={t('选择组类型')}
optionList={typeOptions}
@@ -219,7 +222,7 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
</Col>
<Col span={24}>
<Form.TextArea
field="description"
field='description'
label={t('描述')}
placeholder={t('请输入组描述')}
rows={3}
@@ -229,19 +232,28 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
<Col span={24}>
{selectedType === 'endpoint' ? (
<JSONEditor
field="items"
field='items'
label={t('端点映射')}
value={formRef.current?.getValue('items') ?? (typeof editingGroup?.items === 'string' ? editingGroup.items : JSON.stringify(editingGroup.items || {}, null, 2))}
onChange={(val) => formRef.current?.setValue('items', val)}
value={
formRef.current?.getValue('items') ??
(typeof editingGroup?.items === 'string'
? editingGroup.items
: JSON.stringify(editingGroup.items || {}, null, 2))
}
onChange={(val) =>
formRef.current?.setValue('items', val)
}
editorType='object'
placeholder={'{\n "openai": {"path": "/v1/chat/completions", "method": "POST"}\n}'}
placeholder={
'{\n "openai": {"path": "/v1/chat/completions", "method": "POST"}\n}'
}
template={ENDPOINT_TEMPLATE}
templateLabel={t('填入模板')}
extraText={t('键为端点类型,值为路径和方法对象')}
/>
) : (
<Form.TagInput
field="items"
field='items'
label={t('项目')}
placeholder={t('输入项目名称,按回车添加')}
addOnBlur
@@ -259,4 +271,4 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) =>
);
};
export default EditPrefillGroupModal;
export default EditPrefillGroupModal;

View File

@@ -18,12 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useRef, useEffect } from 'react';
import {
Modal,
Form,
Col,
Row,
} from '@douyinfe/semi-ui';
import { Modal, Form, Col, Row } from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../../../../helpers';
import { Typography } from '@douyinfe/semi-ui';
import { IconLink } from '@douyinfe/semi-icons';
@@ -138,7 +133,7 @@ const EditVendorModal = ({ visible, handleClose, refresh, editingVendor }) => {
<Row gutter={12}>
<Col span={24}>
<Form.Input
field="name"
field='name'
label={t('供应商名称')}
placeholder={t('请输入供应商名称OpenAI')}
rules={[{ required: true, message: t('请输入供应商名称') }]}
@@ -147,7 +142,7 @@ const EditVendorModal = ({ visible, handleClose, refresh, editingVendor }) => {
</Col>
<Col span={24}>
<Form.TextArea
field="description"
field='description'
label={t('描述')}
placeholder={t('请输入供应商描述')}
rows={3}
@@ -156,14 +151,19 @@ const EditVendorModal = ({ visible, handleClose, refresh, editingVendor }) => {
</Col>
<Col span={24}>
<Form.Input
field="icon"
field='icon'
label={t('供应商图标')}
placeholder={t("请输入图标名称")}
placeholder={t('请输入图标名称')}
extraText={
<span>
{t('图标使用@lobehub/icons库OpenAI、Claude.Color支持链式参数OpenAI.Avatar.type={\'platform\'}、OpenRouter.Avatar.shape={\'square\'},查询所有可用图标请 ')}
{t(
"图标使用@lobehub/icons库OpenAI、Claude.Color支持链式参数OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ",
)}
<Typography.Text
link={{ href: 'https://icons.lobehub.com/components/lobe-hub', target: '_blank' }}
link={{
href: 'https://icons.lobehub.com/components/lobe-hub',
target: '_blank',
}}
icon={<IconLink />}
underline
>
@@ -175,11 +175,7 @@ const EditVendorModal = ({ visible, handleClose, refresh, editingVendor }) => {
/>
</Col>
<Col span={24}>
<Form.Switch
field="status"
label={t('状态')}
initValue={true}
/>
<Form.Switch field='status' label={t('状态')} initValue={true} />
</Col>
</Row>
</Form>
@@ -187,4 +183,4 @@ const EditVendorModal = ({ visible, handleClose, refresh, editingVendor }) => {
);
};
export default EditVendorModal;
export default EditVendorModal;

View File

@@ -18,19 +18,25 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
import { Modal, Table, Spin, Button, Typography, Empty, Input } from '@douyinfe/semi-ui';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
import {
Modal,
Table,
Spin,
Button,
Typography,
Empty,
Input,
} from '@douyinfe/semi-ui';
import {
IllustrationNoResult,
IllustrationNoResultDark,
} from '@douyinfe/semi-illustrations';
import { IconSearch } from '@douyinfe/semi-icons';
import { API, showError } from '../../../../helpers';
import { MODEL_TABLE_PAGE_SIZE } from '../../../../constants';
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
const MissingModelsModal = ({
visible,
onClose,
onConfigureModel,
t,
}) => {
const MissingModelsModal = ({ visible, onClose, onConfigureModel, t }) => {
const [loading, setLoading] = useState(false);
const [missingModels, setMissingModels] = useState([]);
const [searchKeyword, setSearchKeyword] = useState('');
@@ -64,7 +70,7 @@ const MissingModelsModal = ({
// 过滤和分页逻辑
const filteredModels = missingModels.filter((model) =>
model.toLowerCase().includes(searchKeyword.toLowerCase())
model.toLowerCase().includes(searchKeyword.toLowerCase()),
);
const dataSource = (() => {
@@ -81,10 +87,10 @@ const MissingModelsModal = ({
title: t('模型名称'),
dataIndex: 'model',
render: (text) => (
<div className="flex items-center">
<div className='flex items-center'>
<Typography.Text strong>{text}</Typography.Text>
</div>
)
),
},
{
title: '',
@@ -93,25 +99,28 @@ const MissingModelsModal = ({
width: 100,
render: (text, record) => (
<Button
type="primary"
size="small"
type='primary'
size='small'
onClick={() => onConfigureModel(record.model)}
>
{t('配置')}
</Button>
)
}
),
},
];
return (
<Modal
title={
<div className="flex flex-col gap-2 w-full">
<div className="flex items-center gap-2">
<Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
<div className='flex flex-col gap-2 w-full'>
<div className='flex items-center gap-2'>
<Typography.Text
strong
className='!text-[var(--semi-color-text-0)] !text-base'
>
{t('未配置的模型列表')}
</Typography.Text>
<Typography.Text type="tertiary" size="small">
<Typography.Text type='tertiary' size='small'>
{t('共')} {missingModels.length} {t('个未配置模型')}
</Typography.Text>
</div>
@@ -121,20 +130,22 @@ const MissingModelsModal = ({
onCancel={onClose}
footer={null}
size={isMobile ? 'full-width' : 'medium'}
className="!rounded-lg"
className='!rounded-lg'
>
<Spin spinning={loading}>
{missingModels.length === 0 && !loading ? (
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
darkModeImage={
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
}
description={t('暂无缺失模型')}
style={{ padding: 30 }}
/>
) : (
<div className="missing-models-content">
<div className='missing-models-content'>
{/* 搜索框 */}
<div className="flex items-center justify-end gap-2 w-full mb-4">
<div className='flex items-center justify-end gap-2 w-full mb-4'>
<Input
placeholder={t('搜索模型...')}
value={searchKeyword}
@@ -142,7 +153,7 @@ const MissingModelsModal = ({
setSearchKeyword(v);
setCurrentPage(1);
}}
className="!w-full"
className='!w-full'
prefix={<IconSearch />}
showClear
/>
@@ -163,9 +174,17 @@ const MissingModelsModal = ({
/>
) : (
<Empty
image={<IllustrationNoResult style={{ width: 100, height: 100 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 100, height: 100 }} />}
description={searchKeyword ? t('未找到匹配的模型') : t('暂无缺失模型')}
image={
<IllustrationNoResult style={{ width: 100, height: 100 }} />
}
darkModeImage={
<IllustrationNoResultDark
style={{ width: 100, height: 100 }}
/>
}
description={
searchKeyword ? t('未找到匹配的模型') : t('暂无缺失模型')
}
style={{ padding: 20 }}
/>
)}

View File

@@ -30,20 +30,25 @@ import {
Spin,
Empty,
} from '@douyinfe/semi-ui';
import {
IconPlus,
IconLayers,
} from '@douyinfe/semi-icons';
import { IconPlus, IconLayers } from '@douyinfe/semi-icons';
import {
IllustrationNoResult,
IllustrationNoResultDark,
} from '@douyinfe/semi-illustrations';
import { API, showError, showSuccess, stringToColor } from '../../../../helpers';
import {
API,
showError,
showSuccess,
stringToColor,
} from '../../../../helpers';
import { useTranslation } from 'react-i18next';
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
import CardTable from '../../../common/ui/CardTable';
import EditPrefillGroupModal from './EditPrefillGroupModal';
import { renderLimitedItems, renderDescription } from '../../../common/ui/RenderUtils';
import {
renderLimitedItems,
renderDescription,
} from '../../../common/ui/RenderUtils';
const { Text, Title } = Typography;
@@ -121,8 +126,9 @@ const PrefillGroupManagement = ({ visible, onClose }) => {
render: (text, record) => (
<Space>
<Text strong>{text}</Text>
<Tag color="white" shape="circle" size="small">
{typeOptions.find(opt => opt.value === record.type)?.label || record.type}
<Tag color='white' shape='circle' size='small'>
{typeOptions.find((opt) => opt.value === record.type)?.label ||
record.type}
</Tag>
</Space>
),
@@ -140,34 +146,49 @@ const PrefillGroupManagement = ({ visible, onClose }) => {
render: (items, record) => {
try {
if (record.type === 'endpoint') {
const obj = typeof items === 'string' ? JSON.parse(items || '{}') : (items || {});
const obj =
typeof items === 'string'
? JSON.parse(items || '{}')
: items || {};
const keys = Object.keys(obj);
if (keys.length === 0) return <Text type="tertiary">{t('暂无项目')}</Text>;
if (keys.length === 0)
return <Text type='tertiary'>{t('暂无项目')}</Text>;
return renderLimitedItems({
items: keys,
renderItem: (key, idx) => (
<Tag key={idx} size="small" shape='circle' color={stringToColor(key)}>
<Tag
key={idx}
size='small'
shape='circle'
color={stringToColor(key)}
>
{key}
</Tag>
),
maxDisplay: 3,
});
}
const itemsArray = typeof items === 'string' ? JSON.parse(items) : items;
const itemsArray =
typeof items === 'string' ? JSON.parse(items) : items;
if (!Array.isArray(itemsArray) || itemsArray.length === 0) {
return <Text type="tertiary">{t('暂无项目')}</Text>;
return <Text type='tertiary'>{t('暂无项目')}</Text>;
}
return renderLimitedItems({
items: itemsArray,
renderItem: (item, idx) => (
<Tag key={idx} size="small" shape='circle' color={stringToColor(item)}>
<Tag
key={idx}
size='small'
shape='circle'
color={stringToColor(item)}
>
{item}
</Tag>
),
maxDisplay: 3,
});
} catch {
return <Text type="tertiary">{t('数据格式错误')}</Text>;
return <Text type='tertiary'>{t('数据格式错误')}</Text>;
}
},
},
@@ -178,20 +199,14 @@ const PrefillGroupManagement = ({ visible, onClose }) => {
width: 140,
render: (_, record) => (
<Space>
<Button
size="small"
onClick={() => handleEdit(record)}
>
<Button size='small' onClick={() => handleEdit(record)}>
{t('编辑')}
</Button>
<Popconfirm
title={t('确定删除此组?')}
onConfirm={() => deleteGroup(record.id)}
>
<Button
size="small"
type="danger"
>
<Button size='small' type='danger'>
{t('删除')}
</Button>
</Popconfirm>
@@ -209,7 +224,7 @@ const PrefillGroupManagement = ({ visible, onClose }) => {
return (
<>
<SideSheet
placement="left"
placement='left'
title={
<Space>
<Tag color='blue' shape='circle'>
@@ -235,14 +250,16 @@ const PrefillGroupManagement = ({ visible, onClose }) => {
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('组列表')}</Text>
<div className='text-xs text-gray-600'>{t('管理模型、标签、端点等预填组')}</div>
<div className='text-xs text-gray-600'>
{t('管理模型、标签、端点等预填组')}
</div>
</div>
</div>
<div className="flex justify-end mb-4">
<div className='flex justify-end mb-4'>
<Button
type="primary"
type='primary'
theme='solid'
size="small"
size='small'
icon={<IconPlus />}
onClick={() => handleEdit()}
>
@@ -253,15 +270,21 @@ const PrefillGroupManagement = ({ visible, onClose }) => {
<CardTable
columns={columns}
dataSource={groups}
rowKey="id"
rowKey='id'
hidePagination={true}
size="small"
size='small'
scroll={{ x: 'max-content' }}
/>
) : (
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
image={
<IllustrationNoResult style={{ width: 150, height: 150 }} />
}
darkModeImage={
<IllustrationNoResultDark
style={{ width: 150, height: 150 }}
/>
}
description={t('暂无预填组')}
style={{ padding: 30 }}
/>
@@ -282,4 +305,4 @@ const PrefillGroupManagement = ({ visible, onClose }) => {
);
};
export default PrefillGroupManagement;
export default PrefillGroupManagement;