Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
c0a9934a
authored
Jan 08, 2026
by
Bliod
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: select model in visualized modl mapping
parent
04ff3df5
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
353 additions
and
21 deletions
+353
-21
web/src/components/common/ui/JSONEditor.jsx
+6
-2
web/src/components/table/channels/modals/EditChannelModal.jsx
+152
-19
web/src/components/table/channels/modals/SingleModelSelectModal.jsx
+195
-0
No files found.
web/src/components/common/ui/JSONEditor.jsx
View file @
c0a9934a
...
@@ -60,6 +60,7 @@ const JSONEditor = ({
...
@@ -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 = ({
...
@@ -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 = ({
...
@@ -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 = ({
...
@@ -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
/>
}
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
c0a9934a
...
@@ -46,6 +46,7 @@ import {
...
@@ -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 {
...
@@ -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 {
...
@@ -69,6 +71,7 @@ import {
IconCode
,
IconCode
,
IconGlobe
,
IconGlobe
,
IconBolt
,
IconBolt
,
IconSearch
,
IconChevronUp
,
IconChevronUp
,
IconChevronDown
,
IconChevronDown
,
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
...
@@ -181,6 +184,13 @@ const EditChannelModal = (props) => {
...
@@ -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) => {
...
@@ -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) => {
...
@@ -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) => {
...
@@ -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
()
=>
{
...
@@ -1677,7 +1724,9 @@ const EditChannelModal = (props) => {
...
@@ -1677,7 +1724,9 @@ const EditChannelModal = (props) => {
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
>
<
Space
>
{
ionetMetadata
?.
deployment_id
&&
(
{
ionetMetadata
?.
deployment_id
&&
(
...
@@ -1754,7 +1803,10 @@ const EditChannelModal = (props) => {
...
@@ -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) => {
...
@@ -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'
,
)
)
...
@@ -1905,7 +1959,9 @@ const EditChannelModal = (props) => {
...
@@ -1905,7 +1959,9 @@ const EditChannelModal = (props) => {
</
Button
>
</
Button
>
<
Button
<
Button
size=
'small'
size=
'small'
type=
{
useManualInput
?
'primary'
:
'tertiary'
}
type=
{
useManualInput
?
'primary'
:
'tertiary'
}
onClick=
{
()
=>
{
onClick=
{
()
=>
{
setUseManualInput
(
true
);
setUseManualInput
(
true
);
// 切换到手动输入模式时清空文件上传相关状态
// 切换到手动输入模式时清空文件上传相关状态
...
@@ -2036,7 +2092,9 @@ const EditChannelModal = (props) => {
...
@@ -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=
{
...
@@ -2424,14 +2482,17 @@ const EditChannelModal = (props) => {
...
@@ -2424,14 +2482,17 @@ const EditChannelModal = (props) => {
label
:
'https://ark.cn-beijing.volces.com'
,
label
:
'https://ark.cn-beijing.volces.com'
,
},
},
{
{
value
:
'https://ark.ap-southeast.bytepluses.com'
,
value
:
label
:
'https://ark.ap-southeast.bytepluses.com'
,
'https://ark.ap-southeast.bytepluses.com'
,
label
:
'https://ark.ap-southeast.bytepluses.com'
,
},
},
{
{
value
:
'doubao-coding-plan'
,
value
:
'doubao-coding-plan'
,
label
:
'Doubao Coding Plan'
,
label
:
'Doubao Coding Plan'
,
},
},
]
}
defaultValue=
'https://ark.cn-beijing.volces.com'
]
}
defaultValue=
'https://ark.cn-beijing.volces.com'
disabled=
{
isIonetLocked
}
disabled=
{
isIonetLocked
}
/>
/>
</
div
>
</
div
>
...
@@ -2577,7 +2638,9 @@ const EditChannelModal = (props) => {
...
@@ -2577,7 +2638,9 @@ const EditChannelModal = (props) => {
try
{
try
{
if
(
Array
.
isArray
(
group
.
items
))
{
if
(
Array
.
isArray
(
group
.
items
))
{
items
=
group
.
items
;
items
=
group
.
items
;
}
else
if
(
typeof
group
.
items
===
'string'
)
{
}
else
if
(
typeof
group
.
items
===
'string'
)
{
const
parsed
=
JSON
.
parse
(
const
parsed
=
JSON
.
parse
(
group
.
items
||
'[]'
,
group
.
items
||
'[]'
,
);
);
...
@@ -2650,6 +2713,27 @@ const EditChannelModal = (props) => {
...
@@ -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) => {
...
@@ -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) => {
...
@@ -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
)
{
...
...
web/src/components/table/channels/modals/SingleModelSelectModal.jsx
0 → 100644
View file @
c0a9934a
/*
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
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment