mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-06 05:09:09 +08:00
refactor(ModelList): improve model list style and grouping (#5674)
* refactor(ModelList): make the group removing button consistent with model removing button * refactor(ModelList): improve auto-grouping, reduce the number of groups * refactor(EditModelsPopup): extract model group tools * refactor(naming): add special grouping rules for some providers * feat(ModelList): add a button to add/remove all the listed models * refactor(naming): update auto grouping * refactor: update auto grouping for dmxapi * revert: remove ungrouped
This commit is contained in:
parent
4c3c863c7d
commit
83cea0750d
@ -1389,6 +1389,8 @@
|
|||||||
"models.default_assistant_model_description": "Model used when creating a new assistant, if the assistant is not set, this model will be used",
|
"models.default_assistant_model_description": "Model used when creating a new assistant, if the assistant is not set, this model will be used",
|
||||||
"models.empty": "No models found",
|
"models.empty": "No models found",
|
||||||
"models.enable_topic_naming": "Topic Auto Naming",
|
"models.enable_topic_naming": "Topic Auto Naming",
|
||||||
|
"models.manage.add_listed": "Add models to the list",
|
||||||
|
"models.manage.remove_listed": "Remove models from the list",
|
||||||
"models.manage.add_whole_group": "Add the whole group",
|
"models.manage.add_whole_group": "Add the whole group",
|
||||||
"models.manage.remove_whole_group": "Remove the whole group",
|
"models.manage.remove_whole_group": "Remove the whole group",
|
||||||
"models.topic_naming_model": "Topic Naming Model",
|
"models.topic_naming_model": "Topic Naming Model",
|
||||||
|
|||||||
@ -1387,6 +1387,8 @@
|
|||||||
"models.default_assistant_model_description": "新しいアシスタントを作成する際に使用されるモデル。アシスタントがモデルを設定していない場合、このモデルが使用されます",
|
"models.default_assistant_model_description": "新しいアシスタントを作成する際に使用されるモデル。アシスタントがモデルを設定していない場合、このモデルが使用されます",
|
||||||
"models.empty": "モデルが見つかりません",
|
"models.empty": "モデルが見つかりません",
|
||||||
"models.enable_topic_naming": "トピックの自動命名",
|
"models.enable_topic_naming": "トピックの自動命名",
|
||||||
|
"models.manage.add_listed": "リストにモデルを追加",
|
||||||
|
"models.manage.remove_listed": "リストからモデルを削除",
|
||||||
"models.manage.add_whole_group": "グループ全体を追加",
|
"models.manage.add_whole_group": "グループ全体を追加",
|
||||||
"models.manage.remove_whole_group": "グループ全体を削除",
|
"models.manage.remove_whole_group": "グループ全体を削除",
|
||||||
"models.topic_naming_model": "トピック命名モデル",
|
"models.topic_naming_model": "トピック命名モデル",
|
||||||
|
|||||||
@ -1387,6 +1387,8 @@
|
|||||||
"models.default_assistant_model_description": "Модель, используемая при создании нового ассистента, если ассистент не имеет настроенной модели, будет использоваться эта модель",
|
"models.default_assistant_model_description": "Модель, используемая при создании нового ассистента, если ассистент не имеет настроенной модели, будет использоваться эта модель",
|
||||||
"models.empty": "Модели не найдены",
|
"models.empty": "Модели не найдены",
|
||||||
"models.enable_topic_naming": "Автоматическое переименование топика",
|
"models.enable_topic_naming": "Автоматическое переименование топика",
|
||||||
|
"models.manage.add_listed": "Добавить в список",
|
||||||
|
"models.manage.remove_listed": "Удалить из списка",
|
||||||
"models.manage.add_whole_group": "Добавить всю группу",
|
"models.manage.add_whole_group": "Добавить всю группу",
|
||||||
"models.manage.remove_whole_group": "Удалить всю группу",
|
"models.manage.remove_whole_group": "Удалить всю группу",
|
||||||
"models.topic_naming_model": "Модель именования топика",
|
"models.topic_naming_model": "Модель именования топика",
|
||||||
|
|||||||
@ -1389,6 +1389,8 @@
|
|||||||
"models.default_assistant_model_description": "创建新助手时使用的模型,如果助手未设置模型,则使用此模型",
|
"models.default_assistant_model_description": "创建新助手时使用的模型,如果助手未设置模型,则使用此模型",
|
||||||
"models.empty": "没有模型",
|
"models.empty": "没有模型",
|
||||||
"models.enable_topic_naming": "话题自动重命名",
|
"models.enable_topic_naming": "话题自动重命名",
|
||||||
|
"models.manage.add_listed": "添加列表中的模型",
|
||||||
|
"models.manage.remove_listed": "移除列表中的模型",
|
||||||
"models.manage.add_whole_group": "添加整个分组",
|
"models.manage.add_whole_group": "添加整个分组",
|
||||||
"models.manage.remove_whole_group": "移除整个分组",
|
"models.manage.remove_whole_group": "移除整个分组",
|
||||||
"models.topic_naming_model": "话题命名模型",
|
"models.topic_naming_model": "话题命名模型",
|
||||||
|
|||||||
@ -1388,6 +1388,8 @@
|
|||||||
"models.default_assistant_model_description": "建立新助手時使用的模型,如果助手未設定模型,則使用此模型",
|
"models.default_assistant_model_description": "建立新助手時使用的模型,如果助手未設定模型,則使用此模型",
|
||||||
"models.empty": "找不到模型",
|
"models.empty": "找不到模型",
|
||||||
"models.enable_topic_naming": "話題自動重新命名",
|
"models.enable_topic_naming": "話題自動重新命名",
|
||||||
|
"models.manage.add_listed": "添加列表中的模型",
|
||||||
|
"models.manage.remove_listed": "移除列表中的模型",
|
||||||
"models.manage.add_whole_group": "新增整個分組",
|
"models.manage.add_whole_group": "新增整個分組",
|
||||||
"models.manage.remove_whole_group": "移除整個分組",
|
"models.manage.remove_whole_group": "移除整個分組",
|
||||||
"models.topic_naming_model": "話題命名模型",
|
"models.topic_naming_model": "話題命名模型",
|
||||||
|
|||||||
@ -104,7 +104,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
|||||||
maxLength={200}
|
maxLength={200}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
form.setFieldValue('name', e.target.value)
|
form.setFieldValue('name', e.target.value)
|
||||||
form.setFieldValue('group', getDefaultGroupName(e.target.value))
|
form.setFieldValue('group', getDefaultGroupName(e.target.value, provider.id))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { Avatar, Button, Empty, Flex, Modal, Tabs, Tooltip, Typography } from 'a
|
|||||||
import Input from 'antd/es/input/Input'
|
import Input from 'antd/es/input/Input'
|
||||||
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
||||||
import { Search } from 'lucide-react'
|
import { Search } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -83,39 +83,36 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const modelGroups =
|
const modelGroups = useMemo(
|
||||||
provider.id === 'dashscope'
|
() =>
|
||||||
? {
|
provider.id === 'dashscope'
|
||||||
...groupBy(
|
? {
|
||||||
list.filter((model) => !model.id.startsWith('qwen')),
|
...groupBy(
|
||||||
'group'
|
list.filter((model) => !model.id.startsWith('qwen')),
|
||||||
),
|
'group'
|
||||||
...groupQwenModels(list.filter((model) => model.id.startsWith('qwen')))
|
),
|
||||||
}
|
...groupQwenModels(list.filter((model) => model.id.startsWith('qwen')))
|
||||||
: groupBy(list, 'group')
|
}
|
||||||
|
: groupBy(list, 'group'),
|
||||||
|
[list, provider.id]
|
||||||
|
)
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = useCallback(() => setOpen(false), [])
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = useCallback(() => setOpen(false), [])
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = useCallback(() => resolve({}), [resolve])
|
||||||
resolve({})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onAddModel = (model: Model) => {
|
const onAddModel = useCallback(
|
||||||
if (isEmpty(model.name)) {
|
(model: Model) => {
|
||||||
return
|
if (!isEmpty(model.name)) {
|
||||||
}
|
addModel(model)
|
||||||
addModel(model)
|
}
|
||||||
}
|
},
|
||||||
|
[addModel]
|
||||||
|
)
|
||||||
|
|
||||||
const onRemoveModel = (model: Model) => {
|
const onRemoveModel = useCallback((model: Model) => removeModel(model), [removeModel])
|
||||||
removeModel(model)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
@ -129,7 +126,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
// @ts-ignore name
|
// @ts-ignore name
|
||||||
name: model.name || model.id,
|
name: model.name || model.id,
|
||||||
provider: _provider.id,
|
provider: _provider.id,
|
||||||
group: getDefaultGroupName(model.id),
|
group: getDefaultGroupName(model.id, _provider.id),
|
||||||
// @ts-ignore name
|
// @ts-ignore name
|
||||||
description: model?.description,
|
description: model?.description,
|
||||||
owned_by: model?.owned_by
|
owned_by: model?.owned_by
|
||||||
@ -165,6 +162,63 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderTopTools = useCallback(() => {
|
||||||
|
const isAllFilteredInProvider = list.length > 0 && list.every((model) => isModelInProvider(provider, model.id))
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
destroyTooltipOnHide
|
||||||
|
title={
|
||||||
|
isAllFilteredInProvider ? t('settings.models.manage.remove_listed') : t('settings.models.manage.add_listed')
|
||||||
|
}
|
||||||
|
placement="top">
|
||||||
|
<Button
|
||||||
|
type={isAllFilteredInProvider ? 'default' : 'primary'}
|
||||||
|
icon={isAllFilteredInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
||||||
|
size="large"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (isAllFilteredInProvider) {
|
||||||
|
list.filter((model) => isModelInProvider(provider, model.id)).forEach(onRemoveModel)
|
||||||
|
} else {
|
||||||
|
list.filter((model) => !isModelInProvider(provider, model.id)).forEach(onAddModel)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={list.length === 0}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}, [list, provider, onAddModel, onRemoveModel, t])
|
||||||
|
|
||||||
|
const renderGroupTools = useCallback(
|
||||||
|
(group: string) => {
|
||||||
|
const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id))
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
destroyTooltipOnHide
|
||||||
|
title={
|
||||||
|
isAllInProvider
|
||||||
|
? t(`settings.models.manage.remove_whole_group`)
|
||||||
|
: t(`settings.models.manage.add_whole_group`)
|
||||||
|
}
|
||||||
|
placement="top">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (isAllInProvider) {
|
||||||
|
modelGroups[group].filter((model) => isModelInProvider(provider, model.id)).forEach(onRemoveModel)
|
||||||
|
} else {
|
||||||
|
modelGroups[group].filter((model) => !isModelInProvider(provider, model.id)).forEach(onAddModel)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[modelGroups, provider, onRemoveModel, onAddModel, t]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={<ModalHeader />}
|
title={<ModalHeader />}
|
||||||
@ -180,14 +234,17 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
}}
|
}}
|
||||||
centered>
|
centered>
|
||||||
<SearchContainer>
|
<SearchContainer>
|
||||||
<Input
|
<TopToolsWrapper>
|
||||||
prefix={<Search size={14} />}
|
<Input
|
||||||
size="large"
|
prefix={<Search size={14} />}
|
||||||
ref={searchInputRef}
|
size="large"
|
||||||
placeholder={t('settings.provider.search_placeholder')}
|
ref={searchInputRef}
|
||||||
allowClear
|
placeholder={t('settings.provider.search_placeholder')}
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
allowClear
|
||||||
/>
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
/>
|
||||||
|
{renderTopTools()}
|
||||||
|
</TopToolsWrapper>
|
||||||
<Tabs
|
<Tabs
|
||||||
size={i18n.language.startsWith('zh') ? 'middle' : 'small'}
|
size={i18n.language.startsWith('zh') ? 'middle' : 'small'}
|
||||||
defaultActiveKey="all"
|
defaultActiveKey="all"
|
||||||
@ -206,7 +263,6 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
</SearchContainer>
|
</SearchContainer>
|
||||||
<ListContainer>
|
<ListContainer>
|
||||||
{Object.keys(modelGroups).map((group, i) => {
|
{Object.keys(modelGroups).map((group, i) => {
|
||||||
const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id))
|
|
||||||
return (
|
return (
|
||||||
<CustomCollapse
|
<CustomCollapse
|
||||||
key={i}
|
key={i}
|
||||||
@ -220,31 +276,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
</CustomTag>
|
</CustomTag>
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
extra={
|
extra={renderGroupTools(group)}>
|
||||||
<Tooltip
|
|
||||||
destroyTooltipOnHide
|
|
||||||
title={
|
|
||||||
isAllInProvider
|
|
||||||
? t(`settings.models.manage.remove_whole_group`)
|
|
||||||
: t(`settings.models.manage.add_whole_group`)
|
|
||||||
}
|
|
||||||
placement="top">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
if (isAllInProvider) {
|
|
||||||
modelGroups[group]
|
|
||||||
.filter((model) => isModelInProvider(provider, model.id))
|
|
||||||
.forEach(onRemoveModel)
|
|
||||||
} else {
|
|
||||||
modelGroups[group].filter((model) => !isModelInProvider(provider, model.id)).forEach(onAddModel)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
}>
|
|
||||||
<FlexColumn style={{ margin: '10px 0' }}>
|
<FlexColumn style={{ margin: '10px 0' }}>
|
||||||
{modelGroups[group].map((model) => (
|
{modelGroups[group].map((model) => (
|
||||||
<FileItem
|
<FileItem
|
||||||
@ -325,6 +357,12 @@ const SearchContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const TopToolsWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
const ListContainer = styled.div`
|
const ListContainer = styled.div`
|
||||||
height: calc(100vh - 300px);
|
height: calc(100vh - 300px);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
CloseCircleFilled,
|
CloseCircleFilled,
|
||||||
ExclamationCircleFilled,
|
ExclamationCircleFilled,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
MinusCircleOutlined,
|
|
||||||
MinusOutlined,
|
MinusOutlined,
|
||||||
PlusOutlined
|
PlusOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
@ -244,77 +243,81 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
|
|||||||
<>
|
<>
|
||||||
<Flex gap={12} vertical>
|
<Flex gap={12} vertical>
|
||||||
{Object.keys(sortedModelGroups).map((group, i) => (
|
{Object.keys(sortedModelGroups).map((group, i) => (
|
||||||
<CustomCollapse
|
<CustomCollapseWrapper key={group}>
|
||||||
defaultActiveKey={i <= 5 ? ['1'] : []}
|
<CustomCollapse
|
||||||
key={group}
|
defaultActiveKey={i <= 5 ? ['1'] : []}
|
||||||
label={
|
label={
|
||||||
<Flex align="center" gap={10}>
|
<Flex align="center" gap={10}>
|
||||||
<span style={{ fontWeight: 600 }}>{group}</span>
|
<span style={{ fontWeight: 600 }}>{group}</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
extra={
|
extra={
|
||||||
<Tooltip title={t('settings.models.manage.remove_whole_group')}>
|
<Tooltip title={t('settings.models.manage.remove_whole_group')}>
|
||||||
<HoveredRemoveIcon
|
<Button
|
||||||
onClick={() =>
|
type="text"
|
||||||
modelGroups[group]
|
className="toolbar-item"
|
||||||
.filter((model) => provider.models.some((m) => m.id === model.id))
|
icon={<MinusOutlined />}
|
||||||
.forEach((model) => removeModel(model))
|
onClick={() =>
|
||||||
}
|
modelGroups[group]
|
||||||
/>
|
.filter((model) => provider.models.some((m) => m.id === model.id))
|
||||||
</Tooltip>
|
.forEach((model) => removeModel(model))
|
||||||
}>
|
}
|
||||||
<Flex gap={10} vertical style={{ marginTop: 10 }}>
|
/>
|
||||||
{sortedModelGroups[group].map((model) => {
|
</Tooltip>
|
||||||
const modelStatus = modelStatuses.find((status) => status.model.id === model.id)
|
}>
|
||||||
const isChecking = modelStatus?.checking === true
|
<Flex gap={10} vertical style={{ marginTop: 10 }}>
|
||||||
|
{sortedModelGroups[group].map((model) => {
|
||||||
|
const modelStatus = modelStatuses.find((status) => status.model.id === model.id)
|
||||||
|
const isChecking = modelStatus?.checking === true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem key={model.id}>
|
<ListItem key={model.id}>
|
||||||
<HStack alignItems="center" gap={10} style={{ flex: 1 }}>
|
<HStack alignItems="center" gap={10} style={{ flex: 1 }}>
|
||||||
<Avatar src={getModelLogo(model.id)} style={{ width: 26, height: 26 }}>
|
<Avatar src={getModelLogo(model.id)} style={{ width: 26, height: 26 }}>
|
||||||
{model?.name?.[0]?.toUpperCase()}
|
{model?.name?.[0]?.toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<ListItemName>
|
<ListItemName>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
maxWidth: '500px'
|
maxWidth: '500px'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
destroyTooltipOnHide
|
||||||
|
title={
|
||||||
|
<Typography.Text style={{ color: 'white' }} copyable={{ text: model.id }}>
|
||||||
|
{model.id}
|
||||||
|
</Typography.Text>
|
||||||
}
|
}
|
||||||
}}
|
placement="top">
|
||||||
destroyTooltipOnHide
|
<NameSpan>{model.name}</NameSpan>
|
||||||
title={
|
</Tooltip>
|
||||||
<Typography.Text style={{ color: 'white' }} copyable={{ text: model.id }}>
|
<ModelTagsWithLabel model={model} size={11} style={{ flexShrink: 0 }} />
|
||||||
{model.id}
|
</ListItemName>
|
||||||
</Typography.Text>
|
</HStack>
|
||||||
}
|
<Flex gap={4} align="center">
|
||||||
placement="top">
|
{renderLatencyText(modelStatus)}
|
||||||
<NameSpan>{model.name}</NameSpan>
|
{renderStatusIndicator(modelStatus)}
|
||||||
</Tooltip>
|
<Button
|
||||||
<ModelTagsWithLabel model={model} size={11} style={{ flexShrink: 0 }} />
|
type="text"
|
||||||
</ListItemName>
|
onClick={() => !isChecking && onEditModel(model)}
|
||||||
</HStack>
|
disabled={isChecking}
|
||||||
<Flex gap={4} align="center">
|
icon={<Bolt size={16} />}
|
||||||
{renderLatencyText(modelStatus)}
|
/>
|
||||||
{renderStatusIndicator(modelStatus)}
|
<Button
|
||||||
<Button
|
type="text"
|
||||||
type="text"
|
onClick={() => !isChecking && removeModel(model)}
|
||||||
onClick={() => !isChecking && onEditModel(model)}
|
disabled={isChecking}
|
||||||
disabled={isChecking}
|
icon={<MinusOutlined />}
|
||||||
icon={<Bolt size={16} />}
|
/>
|
||||||
/>
|
</Flex>
|
||||||
<Button
|
</ListItem>
|
||||||
type="text"
|
)
|
||||||
onClick={() => !isChecking && removeModel(model)}
|
})}
|
||||||
disabled={isChecking}
|
</Flex>
|
||||||
icon={<MinusOutlined />}
|
</CustomCollapse>
|
||||||
/>
|
</CustomCollapseWrapper>
|
||||||
</Flex>
|
|
||||||
</ListItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</CustomCollapse>
|
|
||||||
))}
|
))}
|
||||||
{docsWebsite && (
|
{docsWebsite && (
|
||||||
<SettingHelpTextRow>
|
<SettingHelpTextRow>
|
||||||
@ -352,6 +355,19 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CustomCollapseWrapper = styled.div`
|
||||||
|
.toolbar-item {
|
||||||
|
margin-top: 2px;
|
||||||
|
transform: translateZ(0);
|
||||||
|
will-change: opacity;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
&:hover .toolbar-item {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const ListItem = styled.div`
|
const ListItem = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -387,24 +403,6 @@ const NameSpan = styled.span`
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const RemoveIcon = styled(MinusCircleOutlined)`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 18px;
|
|
||||||
color: var(--color-error);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
`
|
|
||||||
|
|
||||||
const HoveredRemoveIcon = styled(RemoveIcon)`
|
|
||||||
opacity: 0;
|
|
||||||
margin-top: 2px;
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const StatusIndicator = styled.div<{ type: string }>`
|
const StatusIndicator = styled.div<{ type: string }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -115,14 +115,45 @@ describe('naming', () => {
|
|||||||
expect(getDefaultGroupName('group:model')).toBe('group')
|
expect(getDefaultGroupName('group:model')).toBe('group')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should extract group name from ID with space', () => {
|
||||||
|
// 验证从包含空格的 ID 中提取组名
|
||||||
|
expect(getDefaultGroupName('foo bar')).toBe('foo')
|
||||||
|
})
|
||||||
|
|
||||||
it('should extract group name from ID with hyphen', () => {
|
it('should extract group name from ID with hyphen', () => {
|
||||||
// 验证从包含连字符的 ID 中提取组名
|
// 验证从包含连字符的 ID 中提取组名
|
||||||
expect(getDefaultGroupName('group-subgroup-model')).toBe('group-subgroup')
|
expect(getDefaultGroupName('group-subgroup-model')).toBe('group-subgroup')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return original ID if no separators', () => {
|
it('should use first delimiters for special providers', () => {
|
||||||
// 验证没有分隔符时返回原始 ID
|
// 这些 provider 下,'/', ' ', '-', '_', ':' 都属于第一类分隔符,分割后取第0部分
|
||||||
expect(getDefaultGroupName('group')).toBe('group')
|
const specialProviders = ['aihubmix', 'silicon', 'ocoolai', 'o3', 'dmxapi']
|
||||||
|
specialProviders.forEach((provider) => {
|
||||||
|
expect(getDefaultGroupName('Qwen/Qwen3-32B', provider)).toBe('qwen')
|
||||||
|
expect(getDefaultGroupName('gpt-4.1-mini', provider)).toBe('gpt')
|
||||||
|
expect(getDefaultGroupName('gpt-4.1', provider)).toBe('gpt')
|
||||||
|
expect(getDefaultGroupName('gpt_4.1', provider)).toBe('gpt')
|
||||||
|
expect(getDefaultGroupName('DeepSeek Chat', provider)).toBe('deepseek')
|
||||||
|
expect(getDefaultGroupName('foo:bar', provider)).toBe('foo')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use first and second delimiters for default providers', () => {
|
||||||
|
// 默认情况下,'/', ' ', ':' 属于第一类分隔符,'-' '_' 属于第二类
|
||||||
|
expect(getDefaultGroupName('Qwen/Qwen3-32B', 'foobar')).toBe('qwen')
|
||||||
|
expect(getDefaultGroupName('gpt-4.1-mini', 'foobar')).toBe('gpt-4.1')
|
||||||
|
expect(getDefaultGroupName('gpt-4.1', 'foobar')).toBe('gpt-4.1')
|
||||||
|
expect(getDefaultGroupName('DeepSeek Chat', 'foobar')).toBe('deepseek')
|
||||||
|
expect(getDefaultGroupName('foo:bar', 'foobar')).toBe('foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fallback to id if no delimiters', () => {
|
||||||
|
// 没有分隔符时返回 id
|
||||||
|
const specialProviders = ['aihubmix', 'silicon', 'ocoolai', 'o3', 'dmxapi']
|
||||||
|
specialProviders.forEach((provider) => {
|
||||||
|
expect(getDefaultGroupName('o3', provider)).toBe('o3')
|
||||||
|
})
|
||||||
|
expect(getDefaultGroupName('o3', 'openai')).toBe('o3')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +1,49 @@
|
|||||||
/**
|
/**
|
||||||
* 从模型 ID 中提取默认组名。
|
* 从模型 ID 中提取默认组名。
|
||||||
|
* 规则如下:
|
||||||
|
* 1. 第一类分隔规则:以第一个出现的分隔符分割,取第 0 个部分作为组名。
|
||||||
|
* 2. 第二类分隔规则:取前两个部分拼接(如 'a-b-c' 得到 'a-b')。
|
||||||
|
* 3. 其他情况返回 id。
|
||||||
|
*
|
||||||
* 例如:
|
* 例如:
|
||||||
* - 'gpt-3.5-turbo-16k-0613' 转换为 'GPT-3.5-Turbo'
|
* - 'gpt-3.5-turbo-16k-0613' => 'gpt-3.5'
|
||||||
* - 'qwen2:1.5b' 转换为 'QWEN2'。
|
* - 'qwen3:32b' => 'qwen3'
|
||||||
|
* - 'Qwen/Qwen3-32b' => 'qwen'
|
||||||
|
* - 'deepseek-r1' => 'deepseek-r1'
|
||||||
|
* - 'o3' => 'o3'
|
||||||
|
*
|
||||||
* @param id 模型 ID 字符串
|
* @param id 模型 ID 字符串
|
||||||
|
* @param provider 提供商 ID 字符串
|
||||||
* @returns string 提取的组名
|
* @returns string 提取的组名
|
||||||
*/
|
*/
|
||||||
export const getDefaultGroupName = (id: string) => {
|
export const getDefaultGroupName = (id: string, provider?: string) => {
|
||||||
if (id.includes('/')) {
|
const str = id.toLowerCase()
|
||||||
return id.split('/')[0]
|
|
||||||
|
// 定义分隔符
|
||||||
|
let firstDelimiters = ['/', ' ', ':']
|
||||||
|
let secondDelimiters = ['-', '_']
|
||||||
|
|
||||||
|
if (provider && ['aihubmix', 'silicon', 'ocoolai', 'o3', 'dmxapi'].includes(provider.toLowerCase())) {
|
||||||
|
firstDelimiters = ['/', ' ', '-', '_', ':']
|
||||||
|
secondDelimiters = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.includes(':')) {
|
// 第一类分隔规则
|
||||||
return id.split(':')[0]
|
for (const delimiter of firstDelimiters) {
|
||||||
|
if (str.includes(delimiter)) {
|
||||||
|
return str.split(delimiter)[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.includes('-')) {
|
// 第二类分隔规则
|
||||||
const parts = id.split('-')
|
for (const delimiter of secondDelimiters) {
|
||||||
return parts[0] + '-' + parts[1]
|
if (str.includes(delimiter)) {
|
||||||
|
const parts = str.split(delimiter)
|
||||||
|
return parts.length > 1 ? parts[0] + '-' + parts[1] : parts[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return id
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user