mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-19 00:37:28 +00:00
feat: select model in visualized modl mapping
This commit is contained in:
@@ -60,6 +60,7 @@ const JSONEditor = ({
|
|||||||
editorType = 'keyValue',
|
editorType = 'keyValue',
|
||||||
rules = [],
|
rules = [],
|
||||||
formApi = null,
|
formApi = null,
|
||||||
|
renderStringValueSuffix,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -335,7 +336,7 @@ const JSONEditor = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 渲染值输入控件(支持嵌套)
|
// 渲染值输入控件(支持嵌套)
|
||||||
const renderValueInput = (pairId, value) => {
|
const renderValueInput = (pairId, pairKey, value) => {
|
||||||
const valueType = typeof value;
|
const valueType = typeof value;
|
||||||
|
|
||||||
if (valueType === 'boolean') {
|
if (valueType === 'boolean') {
|
||||||
@@ -387,6 +388,7 @@ const JSONEditor = ({
|
|||||||
<Input
|
<Input
|
||||||
placeholder={t('参数值')}
|
placeholder={t('参数值')}
|
||||||
value={String(value)}
|
value={String(value)}
|
||||||
|
suffix={renderStringValueSuffix?.({ pairId, pairKey, value })}
|
||||||
onChange={(newValue) => {
|
onChange={(newValue) => {
|
||||||
let convertedValue = newValue;
|
let convertedValue = newValue;
|
||||||
if (newValue === 'true') convertedValue = true;
|
if (newValue === 'true') convertedValue = true;
|
||||||
@@ -470,7 +472,9 @@ const JSONEditor = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>{renderValueInput(pair.id, pair.value)}</Col>
|
<Col span={12}>
|
||||||
|
{renderValueInput(pair.id, pair.key, pair.value)}
|
||||||
|
</Col>
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconDelete />}
|
icon={<IconDelete />}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Highlight,
|
Highlight,
|
||||||
Input,
|
Input,
|
||||||
|
Tooltip,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
getChannelModels,
|
getChannelModels,
|
||||||
@@ -55,6 +56,7 @@ import {
|
|||||||
selectFilter,
|
selectFilter,
|
||||||
} from '../../../../helpers';
|
} from '../../../../helpers';
|
||||||
import ModelSelectModal from './ModelSelectModal';
|
import ModelSelectModal from './ModelSelectModal';
|
||||||
|
import SingleModelSelectModal from './SingleModelSelectModal';
|
||||||
import OllamaModelModal from './OllamaModelModal';
|
import OllamaModelModal from './OllamaModelModal';
|
||||||
import JSONEditor from '../../../common/ui/JSONEditor';
|
import JSONEditor from '../../../common/ui/JSONEditor';
|
||||||
import SecureVerificationModal from '../../../common/modals/SecureVerificationModal';
|
import SecureVerificationModal from '../../../common/modals/SecureVerificationModal';
|
||||||
@@ -69,6 +71,7 @@ import {
|
|||||||
IconCode,
|
IconCode,
|
||||||
IconGlobe,
|
IconGlobe,
|
||||||
IconBolt,
|
IconBolt,
|
||||||
|
IconSearch,
|
||||||
IconChevronUp,
|
IconChevronUp,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
@@ -109,7 +112,7 @@ function type2secretPrompt(type) {
|
|||||||
case 33:
|
case 33:
|
||||||
return '按照如下格式输入:Ak|Sk|Region';
|
return '按照如下格式输入:Ak|Sk|Region';
|
||||||
case 45:
|
case 45:
|
||||||
return '请输入渠道对应的鉴权密钥, 豆包语音输入:AppId|AccessToken';
|
return '请输入渠道对应的鉴权密钥, 豆包语音输入:AppId|AccessToken';
|
||||||
case 50:
|
case 50:
|
||||||
return '按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey';
|
return '按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey';
|
||||||
case 51:
|
case 51:
|
||||||
@@ -181,6 +184,13 @@ const EditChannelModal = (props) => {
|
|||||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||||
const [modelModalVisible, setModelModalVisible] = useState(false);
|
const [modelModalVisible, setModelModalVisible] = useState(false);
|
||||||
const [fetchedModels, setFetchedModels] = useState([]);
|
const [fetchedModels, setFetchedModels] = useState([]);
|
||||||
|
const [modelMappingValueModalVisible, setModelMappingValueModalVisible] =
|
||||||
|
useState(false);
|
||||||
|
const [modelMappingValueModalModels, setModelMappingValueModalModels] =
|
||||||
|
useState([]);
|
||||||
|
const [modelMappingValueKey, setModelMappingValueKey] = useState('');
|
||||||
|
const [modelMappingValueSelected, setModelMappingValueSelected] =
|
||||||
|
useState('');
|
||||||
const [ollamaModalVisible, setOllamaModalVisible] = useState(false);
|
const [ollamaModalVisible, setOllamaModalVisible] = useState(false);
|
||||||
const formApiRef = useRef(null);
|
const formApiRef = useRef(null);
|
||||||
const [vertexKeys, setVertexKeys] = useState([]);
|
const [vertexKeys, setVertexKeys] = useState([]);
|
||||||
@@ -199,17 +209,11 @@ const EditChannelModal = (props) => {
|
|||||||
if (!trimmed) return [];
|
if (!trimmed) return [];
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmed);
|
const parsed = JSON.parse(trimmed);
|
||||||
if (
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
!parsed ||
|
|
||||||
typeof parsed !== 'object' ||
|
|
||||||
Array.isArray(parsed)
|
|
||||||
) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const values = Object.values(parsed)
|
const values = Object.values(parsed)
|
||||||
.map((value) =>
|
.map((value) => (typeof value === 'string' ? value.trim() : undefined))
|
||||||
typeof value === 'string' ? value.trim() : undefined,
|
|
||||||
)
|
|
||||||
.filter((value) => value);
|
.filter((value) => value);
|
||||||
return Array.from(new Set(values));
|
return Array.from(new Set(values));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -426,7 +430,11 @@ const EditChannelModal = (props) => {
|
|||||||
const isIonetLocked = isIonetChannel && isEdit;
|
const isIonetLocked = isIonetChannel && isEdit;
|
||||||
|
|
||||||
const handleInputChange = (name, value) => {
|
const handleInputChange = (name, value) => {
|
||||||
if (isIonetChannel && isEdit && ['type', 'key', 'base_url'].includes(name)) {
|
if (
|
||||||
|
isIonetChannel &&
|
||||||
|
isEdit &&
|
||||||
|
['type', 'key', 'base_url'].includes(name)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (formApiRef.current) {
|
if (formApiRef.current) {
|
||||||
@@ -730,10 +738,49 @@ const EditChannelModal = (props) => {
|
|||||||
if (!silent) {
|
if (!silent) {
|
||||||
setModelModalVisible(true);
|
setModelModalVisible(true);
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
|
return uniqueModels;
|
||||||
} else {
|
} else {
|
||||||
showError(t('获取模型列表失败'));
|
showError(t('获取模型列表失败'));
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModelMappingValueModal = async ({ pairKey, value }) => {
|
||||||
|
const mappingKey = String(pairKey ?? '').trim();
|
||||||
|
if (!mappingKey) return;
|
||||||
|
|
||||||
|
if (!MODEL_FETCHABLE_TYPES.has(inputs.type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let modelsToUse = fetchedModels;
|
||||||
|
if (!Array.isArray(modelsToUse) || modelsToUse.length === 0) {
|
||||||
|
const fetched = await fetchUpstreamModelList('models', { silent: true });
|
||||||
|
if (Array.isArray(fetched)) {
|
||||||
|
modelsToUse = fetched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(modelsToUse) || modelsToUse.length === 0) {
|
||||||
|
showInfo(t('暂无模型'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedModelsToUse = Array.from(
|
||||||
|
new Set(
|
||||||
|
modelsToUse.map((model) => String(model ?? '').trim()).filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const currentValue = String(value ?? '').trim();
|
||||||
|
|
||||||
|
setModelMappingValueModalModels(normalizedModelsToUse);
|
||||||
|
setModelMappingValueKey(mappingKey);
|
||||||
|
setModelMappingValueSelected(
|
||||||
|
normalizedModelsToUse.includes(currentValue) ? currentValue : '',
|
||||||
|
);
|
||||||
|
setModelMappingValueModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchModels = async () => {
|
const fetchModels = async () => {
|
||||||
@@ -1673,43 +1720,45 @@ const EditChannelModal = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isIonetChannel && (
|
{isIonetChannel && (
|
||||||
<Banner
|
<Banner
|
||||||
type='info'
|
type='info'
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
className='mb-4 rounded-xl'
|
className='mb-4 rounded-xl'
|
||||||
description={t('此渠道由 IO.NET 自动同步,类型、密钥和 API 地址已锁定。')}
|
description={t(
|
||||||
>
|
'此渠道由 IO.NET 自动同步,类型、密钥和 API 地址已锁定。',
|
||||||
<Space>
|
|
||||||
{ionetMetadata?.deployment_id && (
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
theme='light'
|
|
||||||
type='primary'
|
|
||||||
icon={<IconGlobe />}
|
|
||||||
onClick={handleOpenIonetDeployment}
|
|
||||||
>
|
|
||||||
{t('查看关联部署')}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</Space>
|
>
|
||||||
</Banner>
|
<Space>
|
||||||
)}
|
{ionetMetadata?.deployment_id && (
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
theme='light'
|
||||||
|
type='primary'
|
||||||
|
icon={<IconGlobe />}
|
||||||
|
onClick={handleOpenIonetDeployment}
|
||||||
|
>
|
||||||
|
{t('查看关联部署')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Banner>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Select
|
<Form.Select
|
||||||
field='type'
|
field='type'
|
||||||
label={t('类型')}
|
label={t('类型')}
|
||||||
placeholder={t('请选择渠道类型')}
|
placeholder={t('请选择渠道类型')}
|
||||||
rules={[{ required: true, message: t('请选择渠道类型') }]}
|
rules={[{ required: true, message: t('请选择渠道类型') }]}
|
||||||
optionList={channelOptionList}
|
optionList={channelOptionList}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
filter={selectFilter}
|
filter={selectFilter}
|
||||||
autoClearSearchValue={false}
|
autoClearSearchValue={false}
|
||||||
searchPosition='dropdown'
|
searchPosition='dropdown'
|
||||||
onSearch={(value) => setChannelSearchValue(value)}
|
onSearch={(value) => setChannelSearchValue(value)}
|
||||||
renderOptionItem={renderChannelOption}
|
renderOptionItem={renderChannelOption}
|
||||||
onChange={(value) => handleInputChange('type', value)}
|
onChange={(value) => handleInputChange('type', value)}
|
||||||
disabled={isIonetLocked}
|
disabled={isIonetLocked}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{inputs.type === 20 && (
|
{inputs.type === 20 && (
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
@@ -1754,7 +1803,10 @@ const EditChannelModal = (props) => {
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
value={inputs.aws_key_type || 'ak_sk'}
|
value={inputs.aws_key_type || 'ak_sk'}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleChannelOtherSettingsChange('aws_key_type', value);
|
handleChannelOtherSettingsChange(
|
||||||
|
'aws_key_type',
|
||||||
|
value,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
extraText={t(
|
extraText={t(
|
||||||
'AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key',
|
'AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key',
|
||||||
@@ -1834,7 +1886,9 @@ const EditChannelModal = (props) => {
|
|||||||
placeholder={
|
placeholder={
|
||||||
inputs.type === 33
|
inputs.type === 33
|
||||||
? inputs.aws_key_type === 'api_key'
|
? inputs.aws_key_type === 'api_key'
|
||||||
? t('请输入 API Key,一行一个,格式:APIKey|Region')
|
? t(
|
||||||
|
'请输入 API Key,一行一个,格式:APIKey|Region',
|
||||||
|
)
|
||||||
: t(
|
: t(
|
||||||
'请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region',
|
'请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region',
|
||||||
)
|
)
|
||||||
@@ -1849,85 +1903,87 @@ const EditChannelModal = (props) => {
|
|||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
onChange={(value) => handleInputChange('key', value)}
|
onChange={(value) => handleInputChange('key', value)}
|
||||||
disabled={isIonetLocked}
|
disabled={isIonetLocked}
|
||||||
extraText={
|
extraText={
|
||||||
<div className='flex items-center gap-2 flex-wrap'>
|
<div className='flex items-center gap-2 flex-wrap'>
|
||||||
{isEdit &&
|
{isEdit &&
|
||||||
isMultiKeyChannel &&
|
isMultiKeyChannel &&
|
||||||
keyMode === 'append' && (
|
keyMode === 'append' && (
|
||||||
<Text type='warning' size='small'>
|
<Text type='warning' size='small'>
|
||||||
{t(
|
{t(
|
||||||
'追加模式:新密钥将添加到现有密钥列表的末尾',
|
'追加模式:新密钥将添加到现有密钥列表的末尾',
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
{isEdit && (
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='primary'
|
||||||
|
theme='outline'
|
||||||
|
onClick={handleShow2FAModal}
|
||||||
|
>
|
||||||
|
{t('查看密钥')}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isEdit && (
|
{batchExtra}
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
type='primary'
|
|
||||||
theme='outline'
|
|
||||||
onClick={handleShow2FAModal}
|
|
||||||
>
|
|
||||||
{t('查看密钥')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{batchExtra}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
showClear
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{inputs.type === 41 &&
|
|
||||||
(inputs.vertex_key_type || 'json') === 'json' ? (
|
|
||||||
<>
|
|
||||||
{!batch && (
|
|
||||||
<div className='flex items-center justify-between mb-3'>
|
|
||||||
<Text className='text-sm font-medium'>
|
|
||||||
{t('密钥输入方式')}
|
|
||||||
</Text>
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
type={
|
|
||||||
!useManualInput ? 'primary' : 'tertiary'
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setUseManualInput(false);
|
|
||||||
// 切换到文件上传模式时清空手动输入的密钥
|
|
||||||
if (formApiRef.current) {
|
|
||||||
formApiRef.current.setValue('key', '');
|
|
||||||
}
|
|
||||||
handleInputChange('key', '');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('文件上传')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
type={useManualInput ? 'primary' : 'tertiary'}
|
|
||||||
onClick={() => {
|
|
||||||
setUseManualInput(true);
|
|
||||||
// 切换到手动输入模式时清空文件上传相关状态
|
|
||||||
setVertexKeys([]);
|
|
||||||
setVertexFileList([]);
|
|
||||||
if (formApiRef.current) {
|
|
||||||
formApiRef.current.setValue(
|
|
||||||
'vertex_files',
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setInputs((prev) => ({
|
|
||||||
...prev,
|
|
||||||
vertex_files: [],
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('手动输入')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{inputs.type === 41 &&
|
||||||
|
(inputs.vertex_key_type || 'json') === 'json' ? (
|
||||||
|
<>
|
||||||
|
{!batch && (
|
||||||
|
<div className='flex items-center justify-between mb-3'>
|
||||||
|
<Text className='text-sm font-medium'>
|
||||||
|
{t('密钥输入方式')}
|
||||||
|
</Text>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type={
|
||||||
|
!useManualInput ? 'primary' : 'tertiary'
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setUseManualInput(false);
|
||||||
|
// 切换到文件上传模式时清空手动输入的密钥
|
||||||
|
if (formApiRef.current) {
|
||||||
|
formApiRef.current.setValue('key', '');
|
||||||
|
}
|
||||||
|
handleInputChange('key', '');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('文件上传')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type={
|
||||||
|
useManualInput ? 'primary' : 'tertiary'
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setUseManualInput(true);
|
||||||
|
// 切换到手动输入模式时清空文件上传相关状态
|
||||||
|
setVertexKeys([]);
|
||||||
|
setVertexFileList([]);
|
||||||
|
if (formApiRef.current) {
|
||||||
|
formApiRef.current.setValue(
|
||||||
|
'vertex_files',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setInputs((prev) => ({
|
||||||
|
...prev,
|
||||||
|
vertex_files: [],
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('手动输入')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{batch && (
|
{batch && (
|
||||||
<Banner
|
<Banner
|
||||||
@@ -2036,7 +2092,9 @@ const EditChannelModal = (props) => {
|
|||||||
inputs.type === 33
|
inputs.type === 33
|
||||||
? inputs.aws_key_type === 'api_key'
|
? inputs.aws_key_type === 'api_key'
|
||||||
? t('请输入 API Key,格式:APIKey|Region')
|
? t('请输入 API Key,格式:APIKey|Region')
|
||||||
: t('按照如下格式输入:AccessKey|SecretAccessKey|Region')
|
: t(
|
||||||
|
'按照如下格式输入:AccessKey|SecretAccessKey|Region',
|
||||||
|
)
|
||||||
: t(type2secretPrompt(inputs.type))
|
: t(type2secretPrompt(inputs.type))
|
||||||
}
|
}
|
||||||
rules={
|
rules={
|
||||||
@@ -2258,86 +2316,86 @@ const EditChannelModal = (props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{inputs.type === 3 && (
|
{inputs.type === 3 && (
|
||||||
<>
|
<>
|
||||||
<Banner
|
<Banner
|
||||||
type='warning'
|
type='warning'
|
||||||
description={t(
|
description={t(
|
||||||
'2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."',
|
'2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."',
|
||||||
)}
|
|
||||||
className='!rounded-lg'
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Form.Input
|
|
||||||
field='base_url'
|
|
||||||
label='AZURE_OPENAI_ENDPOINT'
|
|
||||||
placeholder={t(
|
|
||||||
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com',
|
|
||||||
)}
|
)}
|
||||||
onChange={(value) =>
|
className='!rounded-lg'
|
||||||
handleInputChange('base_url', value)
|
|
||||||
}
|
|
||||||
showClear
|
|
||||||
disabled={isIonetLocked}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<Form.Input
|
||||||
<Form.Input
|
field='base_url'
|
||||||
field='other'
|
label='AZURE_OPENAI_ENDPOINT'
|
||||||
label={t('默认 API 版本')}
|
placeholder={t(
|
||||||
placeholder={t(
|
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com',
|
||||||
'请输入默认 API 版本,例如:2025-04-01-preview',
|
)}
|
||||||
)}
|
onChange={(value) =>
|
||||||
onChange={(value) =>
|
handleInputChange('base_url', value)
|
||||||
handleInputChange('other', value)
|
}
|
||||||
}
|
showClear
|
||||||
showClear
|
disabled={isIonetLocked}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field='azure_responses_version'
|
field='other'
|
||||||
label={t(
|
label={t('默认 API 版本')}
|
||||||
'默认 Responses API 版本,为空则使用上方版本',
|
placeholder={t(
|
||||||
)}
|
'请输入默认 API 版本,例如:2025-04-01-preview',
|
||||||
placeholder={t('例如:preview')}
|
)}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
handleChannelOtherSettingsChange(
|
handleInputChange('other', value)
|
||||||
'azure_responses_version',
|
}
|
||||||
value,
|
showClear
|
||||||
)
|
/>
|
||||||
}
|
</div>
|
||||||
showClear
|
<div>
|
||||||
/>
|
<Form.Input
|
||||||
</div>
|
field='azure_responses_version'
|
||||||
</>
|
label={t(
|
||||||
)}
|
'默认 Responses API 版本,为空则使用上方版本',
|
||||||
|
)}
|
||||||
|
placeholder={t('例如:preview')}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleChannelOtherSettingsChange(
|
||||||
|
'azure_responses_version',
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{inputs.type === 8 && (
|
{inputs.type === 8 && (
|
||||||
<>
|
<>
|
||||||
<Banner
|
<Banner
|
||||||
type='warning'
|
type='warning'
|
||||||
description={t(
|
description={t(
|
||||||
'如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。',
|
'如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。',
|
||||||
)}
|
|
||||||
className='!rounded-lg'
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Form.Input
|
|
||||||
field='base_url'
|
|
||||||
label={t('完整的 Base URL,支持变量{model}')}
|
|
||||||
placeholder={t(
|
|
||||||
'请输入完整的URL,例如:https://api.openai.com/v1/chat/completions',
|
|
||||||
)}
|
)}
|
||||||
onChange={(value) =>
|
className='!rounded-lg'
|
||||||
handleInputChange('base_url', value)
|
|
||||||
}
|
|
||||||
showClear
|
|
||||||
disabled={isIonetLocked}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div>
|
||||||
</>
|
<Form.Input
|
||||||
)}
|
field='base_url'
|
||||||
|
label={t('完整的 Base URL,支持变量{model}')}
|
||||||
|
placeholder={t(
|
||||||
|
'请输入完整的URL,例如:https://api.openai.com/v1/chat/completions',
|
||||||
|
)}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleInputChange('base_url', value)
|
||||||
|
}
|
||||||
|
showClear
|
||||||
|
disabled={isIonetLocked}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{inputs.type === 37 && (
|
{inputs.type === 37 && (
|
||||||
<Banner
|
<Banner
|
||||||
@@ -2366,76 +2424,79 @@ const EditChannelModal = (props) => {
|
|||||||
}
|
}
|
||||||
showClear
|
showClear
|
||||||
disabled={isIonetLocked}
|
disabled={isIonetLocked}
|
||||||
extraText={t(
|
extraText={t(
|
||||||
'对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写',
|
'对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{inputs.type === 22 && (
|
||||||
|
<div>
|
||||||
|
<Form.Input
|
||||||
|
field='base_url'
|
||||||
|
label={t('私有部署地址')}
|
||||||
|
placeholder={t(
|
||||||
|
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi',
|
||||||
)}
|
)}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleInputChange('base_url', value)
|
||||||
|
}
|
||||||
|
showClear
|
||||||
|
disabled={isIonetLocked}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{inputs.type === 22 && (
|
{inputs.type === 36 && (
|
||||||
<div>
|
<div>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field='base_url'
|
field='base_url'
|
||||||
label={t('私有部署地址')}
|
label={t(
|
||||||
placeholder={t(
|
'注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用',
|
||||||
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi',
|
)}
|
||||||
)}
|
placeholder={t(
|
||||||
onChange={(value) =>
|
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com',
|
||||||
handleInputChange('base_url', value)
|
)}
|
||||||
}
|
onChange={(value) =>
|
||||||
showClear
|
|
||||||
disabled={isIonetLocked}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{inputs.type === 36 && (
|
|
||||||
<div>
|
|
||||||
<Form.Input
|
|
||||||
field='base_url'
|
|
||||||
label={t(
|
|
||||||
'注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用',
|
|
||||||
)}
|
|
||||||
placeholder={t(
|
|
||||||
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com',
|
|
||||||
)}
|
|
||||||
onChange={(value) =>
|
|
||||||
handleInputChange('base_url', value)
|
|
||||||
}
|
|
||||||
showClear
|
|
||||||
disabled={isIonetLocked}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{inputs.type === 45 && !doubaoApiEditUnlocked && (
|
|
||||||
<div>
|
|
||||||
<Form.Select
|
|
||||||
field='base_url'
|
|
||||||
label={t('API地址')}
|
|
||||||
placeholder={t('请选择API地址')}
|
|
||||||
onChange={(value) =>
|
|
||||||
handleInputChange('base_url', value)
|
handleInputChange('base_url', value)
|
||||||
}
|
}
|
||||||
optionList={[
|
showClear
|
||||||
{
|
disabled={isIonetLocked}
|
||||||
value: 'https://ark.cn-beijing.volces.com',
|
/>
|
||||||
label: 'https://ark.cn-beijing.volces.com',
|
</div>
|
||||||
},
|
)}
|
||||||
{
|
|
||||||
value: 'https://ark.ap-southeast.bytepluses.com',
|
{inputs.type === 45 && !doubaoApiEditUnlocked && (
|
||||||
label: 'https://ark.ap-southeast.bytepluses.com',
|
<div>
|
||||||
},
|
<Form.Select
|
||||||
{
|
field='base_url'
|
||||||
value: 'doubao-coding-plan',
|
label={t('API地址')}
|
||||||
label: 'Doubao Coding Plan',
|
placeholder={t('请选择API地址')}
|
||||||
},
|
onChange={(value) =>
|
||||||
]}defaultValue='https://ark.cn-beijing.volces.com'
|
handleInputChange('base_url', value)
|
||||||
disabled={isIonetLocked}
|
}
|
||||||
/>
|
optionList={[
|
||||||
</div>
|
{
|
||||||
)}
|
value: 'https://ark.cn-beijing.volces.com',
|
||||||
|
label: 'https://ark.cn-beijing.volces.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value:
|
||||||
|
'https://ark.ap-southeast.bytepluses.com',
|
||||||
|
label:
|
||||||
|
'https://ark.ap-southeast.bytepluses.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'doubao-coding-plan',
|
||||||
|
label: 'Doubao Coding Plan',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
defaultValue='https://ark.cn-beijing.volces.com'
|
||||||
|
disabled={isIonetLocked}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2531,79 +2592,81 @@ const EditChannelModal = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{inputs.type === 4 && isEdit && (
|
{inputs.type === 4 && isEdit && (
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
type='primary'
|
|
||||||
theme='light'
|
|
||||||
onClick={() => setOllamaModalVisible(true)}
|
|
||||||
>
|
|
||||||
{t('Ollama 模型管理')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
type='warning'
|
|
||||||
onClick={() => handleInputChange('models', [])}
|
|
||||||
>
|
|
||||||
{t('清除所有模型')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
type='tertiary'
|
|
||||||
onClick={() => {
|
|
||||||
if (inputs.models.length === 0) {
|
|
||||||
showInfo(t('没有模型可以复制'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
copy(inputs.models.join(','));
|
|
||||||
showSuccess(t('模型列表已复制到剪贴板'));
|
|
||||||
} catch (error) {
|
|
||||||
showError(t('复制失败'));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('复制所有模型')}
|
|
||||||
</Button>
|
|
||||||
{modelGroups &&
|
|
||||||
modelGroups.length > 0 &&
|
|
||||||
modelGroups.map((group) => (
|
|
||||||
<Button
|
<Button
|
||||||
key={group.id}
|
|
||||||
size='small'
|
size='small'
|
||||||
type='primary'
|
type='primary'
|
||||||
onClick={() => {
|
theme='light'
|
||||||
let items = [];
|
onClick={() => setOllamaModalVisible(true)}
|
||||||
try {
|
|
||||||
if (Array.isArray(group.items)) {
|
|
||||||
items = group.items;
|
|
||||||
} else if (typeof group.items === 'string') {
|
|
||||||
const parsed = JSON.parse(
|
|
||||||
group.items || '[]',
|
|
||||||
);
|
|
||||||
if (Array.isArray(parsed)) items = parsed;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
const current =
|
|
||||||
formApiRef.current?.getValue('models') ||
|
|
||||||
inputs.models ||
|
|
||||||
[];
|
|
||||||
const merged = Array.from(
|
|
||||||
new Set(
|
|
||||||
[...current, ...items]
|
|
||||||
.map((m) => (m || '').trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
handleInputChange('models', merged);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{group.name}
|
{t('Ollama 模型管理')}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
)}
|
||||||
</Space>
|
<Button
|
||||||
}
|
size='small'
|
||||||
/>
|
type='warning'
|
||||||
|
onClick={() => handleInputChange('models', [])}
|
||||||
|
>
|
||||||
|
{t('清除所有模型')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='tertiary'
|
||||||
|
onClick={() => {
|
||||||
|
if (inputs.models.length === 0) {
|
||||||
|
showInfo(t('没有模型可以复制'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
copy(inputs.models.join(','));
|
||||||
|
showSuccess(t('模型列表已复制到剪贴板'));
|
||||||
|
} catch (error) {
|
||||||
|
showError(t('复制失败'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('复制所有模型')}
|
||||||
|
</Button>
|
||||||
|
{modelGroups &&
|
||||||
|
modelGroups.length > 0 &&
|
||||||
|
modelGroups.map((group) => (
|
||||||
|
<Button
|
||||||
|
key={group.id}
|
||||||
|
size='small'
|
||||||
|
type='primary'
|
||||||
|
onClick={() => {
|
||||||
|
let items = [];
|
||||||
|
try {
|
||||||
|
if (Array.isArray(group.items)) {
|
||||||
|
items = group.items;
|
||||||
|
} else if (
|
||||||
|
typeof group.items === 'string'
|
||||||
|
) {
|
||||||
|
const parsed = JSON.parse(
|
||||||
|
group.items || '[]',
|
||||||
|
);
|
||||||
|
if (Array.isArray(parsed)) items = parsed;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
const current =
|
||||||
|
formApiRef.current?.getValue('models') ||
|
||||||
|
inputs.models ||
|
||||||
|
[];
|
||||||
|
const merged = Array.from(
|
||||||
|
new Set(
|
||||||
|
[...current, ...items]
|
||||||
|
.map((m) => (m || '').trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
handleInputChange('models', merged);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{group.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field='custom_model'
|
field='custom_model'
|
||||||
@@ -2650,6 +2713,27 @@ const EditChannelModal = (props) => {
|
|||||||
templateLabel={t('填入模板')}
|
templateLabel={t('填入模板')}
|
||||||
editorType='keyValue'
|
editorType='keyValue'
|
||||||
formApi={formApiRef.current}
|
formApi={formApiRef.current}
|
||||||
|
renderStringValueSuffix={({ pairKey, value }) => {
|
||||||
|
if (!MODEL_FETCHABLE_TYPES.has(inputs.type)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const disabled = !String(pairKey ?? '').trim();
|
||||||
|
return (
|
||||||
|
<Tooltip content={t('选择模型')}>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
theme='borderless'
|
||||||
|
size='small'
|
||||||
|
icon={<IconSearch size={14} />}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openModelMappingValueModal({ pairKey, value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}}
|
||||||
extraText={t(
|
extraText={t(
|
||||||
'键为请求中的模型名称,值为要替换的模型名称',
|
'键为请求中的模型名称,值为要替换的模型名称',
|
||||||
)}
|
)}
|
||||||
@@ -3164,6 +3248,53 @@ const EditChannelModal = (props) => {
|
|||||||
onCancel={() => setModelModalVisible(false)}
|
onCancel={() => setModelModalVisible(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SingleModelSelectModal
|
||||||
|
visible={modelMappingValueModalVisible}
|
||||||
|
models={modelMappingValueModalModels}
|
||||||
|
selected={modelMappingValueSelected}
|
||||||
|
onConfirm={(selectedModel) => {
|
||||||
|
const modelName = String(selectedModel ?? '').trim();
|
||||||
|
if (!modelName) {
|
||||||
|
showError(t('请先选择模型!'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappingKey = String(modelMappingValueKey ?? '').trim();
|
||||||
|
if (!mappingKey) {
|
||||||
|
setModelMappingValueModalVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = {};
|
||||||
|
const currentMapping = inputs.model_mapping;
|
||||||
|
if (typeof currentMapping === 'string' && currentMapping.trim()) {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(currentMapping);
|
||||||
|
} catch (error) {
|
||||||
|
parsed = {};
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
currentMapping &&
|
||||||
|
typeof currentMapping === 'object' &&
|
||||||
|
!Array.isArray(currentMapping)
|
||||||
|
) {
|
||||||
|
parsed = currentMapping;
|
||||||
|
}
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
parsed = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed[mappingKey] = modelName;
|
||||||
|
const nextMapping = JSON.stringify(parsed, null, 2);
|
||||||
|
handleInputChange('model_mapping', nextMapping);
|
||||||
|
if (formApiRef.current) {
|
||||||
|
formApiRef.current.setValue('model_mapping', nextMapping);
|
||||||
|
}
|
||||||
|
setModelMappingValueModalVisible(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => setModelMappingValueModalVisible(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
<OllamaModelModal
|
<OllamaModelModal
|
||||||
visible={ollamaModalVisible}
|
visible={ollamaModalVisible}
|
||||||
onCancel={() => setOllamaModalVisible(false)}
|
onCancel={() => setOllamaModalVisible(false)}
|
||||||
@@ -3181,7 +3312,9 @@ const EditChannelModal = (props) => {
|
|||||||
? inputs.models.map(String)
|
? inputs.models.map(String)
|
||||||
: [];
|
: [];
|
||||||
const incoming = modelIds.map(String);
|
const incoming = modelIds.map(String);
|
||||||
const nextModels = Array.from(new Set([...existingModels, ...incoming]));
|
const nextModels = Array.from(
|
||||||
|
new Set([...existingModels, ...incoming]),
|
||||||
|
);
|
||||||
|
|
||||||
handleInputChange('models', nextModels);
|
handleInputChange('models', nextModels);
|
||||||
if (formApiRef.current) {
|
if (formApiRef.current) {
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||||
|
import {
|
||||||
|
Collapse,
|
||||||
|
Empty,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Radio,
|
||||||
|
Typography,
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
IllustrationNoResult,
|
||||||
|
IllustrationNoResultDark,
|
||||||
|
} from '@douyinfe/semi-illustrations';
|
||||||
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
|
import { getModelCategories } from '../../../../helpers/render';
|
||||||
|
|
||||||
|
const SingleModelSelectModal = ({
|
||||||
|
visible,
|
||||||
|
models = [],
|
||||||
|
selected = '',
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const normalizeModelName = (model) => String(model ?? '').trim();
|
||||||
|
const normalizedModels = useMemo(() => {
|
||||||
|
const list = Array.isArray(models) ? models : [];
|
||||||
|
return Array.from(new Set(list.map(normalizeModelName).filter(Boolean)));
|
||||||
|
}, [models]);
|
||||||
|
|
||||||
|
const [keyword, setKeyword] = useState('');
|
||||||
|
const [selectedModel, setSelectedModel] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
setKeyword('');
|
||||||
|
setSelectedModel(normalizeModelName(selected));
|
||||||
|
}
|
||||||
|
}, [visible, selected]);
|
||||||
|
|
||||||
|
const filteredModels = useMemo(() => {
|
||||||
|
const lower = keyword.trim().toLowerCase();
|
||||||
|
if (!lower) return normalizedModels;
|
||||||
|
return normalizedModels.filter((m) => m.toLowerCase().includes(lower));
|
||||||
|
}, [normalizedModels, keyword]);
|
||||||
|
|
||||||
|
const modelsByCategory = useMemo(() => {
|
||||||
|
const categories = getModelCategories(t);
|
||||||
|
const categorized = {};
|
||||||
|
const uncategorized = [];
|
||||||
|
|
||||||
|
filteredModels.forEach((model) => {
|
||||||
|
let foundCategory = false;
|
||||||
|
for (const [key, category] of Object.entries(categories)) {
|
||||||
|
if (key !== 'all' && category.filter({ model_name: model })) {
|
||||||
|
if (!categorized[key]) {
|
||||||
|
categorized[key] = {
|
||||||
|
label: category.label,
|
||||||
|
icon: category.icon,
|
||||||
|
models: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
categorized[key].models.push(model);
|
||||||
|
foundCategory = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundCategory) {
|
||||||
|
uncategorized.push(model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uncategorized.length > 0) {
|
||||||
|
categorized.other = {
|
||||||
|
label: t('其他'),
|
||||||
|
icon: null,
|
||||||
|
models: uncategorized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return categorized;
|
||||||
|
}, [filteredModels, t]);
|
||||||
|
|
||||||
|
const categoryEntries = useMemo(
|
||||||
|
() => Object.entries(modelsByCategory),
|
||||||
|
[modelsByCategory],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
header={
|
||||||
|
<div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4 py-4'>
|
||||||
|
<Typography.Title heading={5} className='m-0'>
|
||||||
|
{t('选择模型')}
|
||||||
|
</Typography.Title>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
visible={visible}
|
||||||
|
onOk={() => onConfirm?.(selectedModel)}
|
||||||
|
onCancel={onCancel}
|
||||||
|
okText={t('确定')}
|
||||||
|
cancelText={t('取消')}
|
||||||
|
okButtonProps={{ disabled: !selectedModel }}
|
||||||
|
size={isMobile ? 'full-width' : 'large'}
|
||||||
|
closeOnEsc
|
||||||
|
maskClosable
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<IconSearch size={14} />}
|
||||||
|
placeholder={t('搜索模型')}
|
||||||
|
value={keyword}
|
||||||
|
onChange={(v) => setKeyword(v)}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ maxHeight: 400, overflowY: 'auto', paddingRight: 8 }}>
|
||||||
|
{filteredModels.length === 0 ? (
|
||||||
|
<Empty
|
||||||
|
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||||
|
darkModeImage={
|
||||||
|
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||||
|
}
|
||||||
|
description={t('暂无匹配模型')}
|
||||||
|
style={{ padding: 30 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Radio.Group
|
||||||
|
className='w-full'
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={selectedModel}
|
||||||
|
onChange={(val) => {
|
||||||
|
const next = val && val.target ? val.target.value : val;
|
||||||
|
setSelectedModel(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
className='w-full'
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
defaultActiveKey={[]}
|
||||||
|
>
|
||||||
|
{categoryEntries.map(([key, categoryData], index) => (
|
||||||
|
<Collapse.Panel
|
||||||
|
key={`${key}_${index}`}
|
||||||
|
itemKey={`${key}_${index}`}
|
||||||
|
header={
|
||||||
|
<span className='flex items-center gap-2'>
|
||||||
|
{categoryData.icon}
|
||||||
|
<span>
|
||||||
|
{categoryData.label} ({categoryData.models.length})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-2 gap-x-4'>
|
||||||
|
{categoryData.models.map((model) => (
|
||||||
|
<Radio key={model} value={model} className='my-1'>
|
||||||
|
{model}
|
||||||
|
</Radio>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Collapse.Panel>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
</Radio.Group>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SingleModelSelectModal;
|
||||||
Reference in New Issue
Block a user