refactor(ModelEditContent): improve experience when choosing model types (#8847)

* feat(标签组件): 新增多种模型标签组件并重构标签引用路径

新增RerankerTag、EmbeddingTag、ReasoningTag、VisionTag和ToolsCallingTag组件
将CustomTag移动至Tags目录并更新所有引用路径
重构ModelTagsWithLabel组件使用新的标签组件

* feat(标签组件): 导出CustomTagProps并增强所有标签组件的props传递

- 导出CustomTagProps接口供其他组件使用
- 在所有标签组件中添加...restProps以支持更多自定义属性
- 新增WebSearchTag组件
- 统一各标签组件的props类型定义方式

* refactor(组件): 统一标签组件的showLabel属性命名

将shouldShowLabel重命名为showLabel以保持命名一致性

* feat(Tags): 为 CustomTag 组件添加 disabled 状态支持

当 disabled 为 true 时,标签颜色将变为灰色

* feat(Tags): 为 CustomTag 组件添加 onClick 事件支持并修复关闭事件冒泡

添加 onClick 属性以支持标签点击事件
修复关闭按钮点击事件冒泡问题

* fix(Tags): 修复CustomTag组件点击状态样式问题

添加$clickable属性以控制鼠标指针样式
确保当onClick存在时显示手型指针

* refactor(ProviderSettings): 替换复选框为标签组件展示模型能力

移除旧的复选框实现,改用专用标签组件展示模型能力类型
简化相关逻辑代码,提升可维护性
调整模态框宽度为自适应内容

* refactor(ProviderSettings): 重构模型编辑弹窗的布局和样式

将模型能力选择部分移动到顶部并优化布局
移除重复的类型标题并调整按钮位置
统一模态框宽度为固定值

* fix(ProviderSettings): 将 Space.Compact 替换为 Space 以修复布局问题

* feat(模型设置): 添加模型类型选择警告提示并优化交互

新增 WarnTooltip 组件用于显示模型类型选择的警告信息
修改模型类型选择交互逻辑,允许用户切换 vision 类型
更新中文翻译文本,使警告信息更准确

* refactor(components): 重构模型能力标签组件并集中管理

将分散的模型能力标签组件移动到统一的 ModelCapabilities 目录
新增 WebSearchTag 组件并优化现有标签组件结构

* feat(组件): 新增带有警告图标的Tooltip组件

* refactor(ProviderSettings): 优化模型能力标签的交互逻辑和性能

使用useMemo和useCallback优化模型类型选择和计算逻辑
重构标签组件导入路径和交互方式

* feat(Tags): 为 CustomTag 组件添加 style 属性支持

允许通过 style 属性自定义标签的样式,提供更灵活的样式控制

* refactor(ProviderSettings): 优化模型类型选择逻辑和UI交互

- 移除冗余代码并简化模型能力选择逻辑
- 添加互斥类型检查防止同时选择不兼容的模型类型
- 为重置按钮添加图标和工具提示提升用户体验
- 统一所有类型标签的禁用状态样式

* fix(ProviderSettings): 为重置按钮添加type="text"属性以修复样式问题

* refactor(组件): 移除GlobalOutlined图标并使用WebSearchTag组件替代

简化WebSearch模型的标签显示逻辑,使用统一的WebSearchTag组件替代手动创建的CustomTag,提高代码复用性和可维护性

* fix(组件): 更换deprecated属性

* feat(组件): 为CustomTag添加inactive状态并优化禁用逻辑

为CustomTag组件新增inactive属性,用于控制标签的视觉禁用状态
将disabled属性与点击事件解耦,优化禁用状态下的交互行为
更新相关调用代码以适配新的属性结构

* style(ProviderSettings): 调整 ModelEditContent 组件中 Flex 布局的换行属性

* fix(ProviderSettings): 移除Modal组件中固定的width属性

* fix(components): 为WebSearchTag组件添加size属性以保持一致性

* fix(ProviderSettings): 使用uniqueObjectArray防止模型能力重复

确保模型能力列表中的项唯一,避免重复添加相同类型的模型能力
This commit is contained in:
Phantom 2025-08-07 20:41:21 +08:00 committed by GitHub
parent ad0c2a11f3
commit 201fcf9f45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 316 additions and 212 deletions

View File

@ -78,7 +78,7 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
style={collapseStyle} style={collapseStyle}
defaultActiveKey={defaultActiveKey} defaultActiveKey={defaultActiveKey}
activeKey={activeKey} activeKey={activeKey}
destroyInactivePanel={destroyInactivePanel} destroyOnHidden={destroyInactivePanel}
collapsible={collapsible} collapsible={collapsible}
onChange={(keys) => { onChange={(keys) => {
setActiveKeys(keys) setActiveKeys(keys)

View File

@ -26,7 +26,7 @@ const ModelIdWithTags = ({
maxWidth: '500px' maxWidth: '500px'
} }
}} }}
destroyTooltipOnHide destroyOnHidden
title={ title={
<Typography.Text style={{ color: 'white' }} copyable={{ text: model.id }}> <Typography.Text style={{ color: 'white' }} copyable={{ text: model.id }}>
{model.id} {model.id}

View File

@ -1,4 +1,3 @@
import { EyeOutlined, GlobalOutlined, ToolOutlined } from '@ant-design/icons'
import { import {
isEmbeddingModel, isEmbeddingModel,
isFunctionCallingModel, isFunctionCallingModel,
@ -14,7 +13,15 @@ import { FC, memo, useLayoutEffect, 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'
import CustomTag from './CustomTag' import CustomTag from './Tags/CustomTag'
import {
EmbeddingTag,
ReasoningTag,
RerankerTag,
ToolsCallingTag,
VisionTag,
WebSearchTag
} from './Tags/ModelCapabilities'
interface ModelTagsProps { interface ModelTagsProps {
model: Model model: Model
@ -70,45 +77,17 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
return ( return (
<Container ref={containerRef} style={style}> <Container ref={containerRef} style={style}>
{isVisionModel(model) && ( {isVisionModel(model) && <VisionTag size={size} showTooltip={showTooltip} showLabel={shouldShowLabel} />}
<CustomTag {isWebSearchModel(model) && <WebSearchTag size={size} showTooltip={showTooltip} showLabel={shouldShowLabel} />}
size={size}
color="#00b96b"
icon={<EyeOutlined style={{ fontSize: size }} />}
tooltip={showTooltip ? t('models.type.vision') : undefined}>
{shouldShowLabel ? t('models.type.vision') : ''}
</CustomTag>
)}
{isWebSearchModel(model) && (
<CustomTag
size={size}
color="#1677ff"
icon={<GlobalOutlined style={{ fontSize: size }} />}
tooltip={showTooltip ? t('models.type.websearch') : undefined}>
{shouldShowLabel ? t('models.type.websearch') : ''}
</CustomTag>
)}
{showReasoning && isReasoningModel(model) && ( {showReasoning && isReasoningModel(model) && (
<CustomTag <ReasoningTag size={size} showTooltip={showTooltip} showLabel={shouldShowLabel} />
size={size}
color="#6372bd"
icon={<i className="iconfont icon-thinking" />}
tooltip={showTooltip ? t('models.type.reasoning') : undefined}>
{shouldShowLabel ? t('models.type.reasoning') : ''}
</CustomTag>
)} )}
{showToolsCalling && isFunctionCallingModel(model) && ( {showToolsCalling && isFunctionCallingModel(model) && (
<CustomTag <ToolsCallingTag size={size} showTooltip={showTooltip} showLabel={shouldShowLabel} />
size={size}
color="#f18737"
icon={<ToolOutlined style={{ fontSize: size }} />}
tooltip={showTooltip ? t('models.type.function_calling') : undefined}>
{shouldShowLabel ? t('models.type.function_calling') : ''}
</CustomTag>
)} )}
{isEmbeddingModel(model) && <CustomTag size={size} color="#FFA500" icon={t('models.type.embedding')} />} {isEmbeddingModel(model) && <EmbeddingTag size={size} />}
{showFree && isFreeModel(model) && <CustomTag size={size} color="#7cb305" icon={t('models.type.free')} />} {showFree && isFreeModel(model) && <CustomTag size={size} color="#7cb305" icon={t('models.type.free')} />}
{isRerankModel(model) && <CustomTag size={size} color="#6495ED" icon={t('models.type.rerank')} />} {isRerankModel(model) && <RerankerTag size={size} />}
</Container> </Container>
) )
} }

View File

@ -1,5 +1,5 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/Tags/CustomTag'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { useKnowledge, useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useKnowledge, useKnowledgeBases } from '@renderer/hooks/useKnowledge'
import { Message } from '@renderer/types/newMessage' import { Message } from '@renderer/types/newMessage'

View File

@ -1,27 +1,58 @@
import { CloseOutlined } from '@ant-design/icons' import { CloseOutlined } from '@ant-design/icons'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { FC, memo, useMemo } from 'react' import { CSSProperties, FC, memo, useMemo } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
interface CustomTagProps { export interface CustomTagProps {
icon?: React.ReactNode icon?: React.ReactNode
children?: React.ReactNode | string children?: React.ReactNode | string
color: string color: string
size?: number size?: number
style?: CSSProperties
tooltip?: string tooltip?: string
closable?: boolean closable?: boolean
onClose?: () => void onClose?: () => void
onClick?: () => void
disabled?: boolean
inactive?: boolean
} }
const CustomTag: FC<CustomTagProps> = ({ children, icon, color, size = 12, tooltip, closable = false, onClose }) => { const CustomTag: FC<CustomTagProps> = ({
children,
icon,
color,
size = 12,
style,
tooltip,
closable = false,
onClose,
onClick,
disabled,
inactive
}) => {
const actualColor = inactive ? '#aaaaaa' : color
const tagContent = useMemo( const tagContent = useMemo(
() => ( () => (
<Tag $color={color} $size={size} $closable={closable}> <Tag
$color={actualColor}
$size={size}
$closable={closable}
onClick={disabled ? undefined : onClick}
style={{ cursor: disabled ? 'not-allowed' : onClick ? 'pointer' : 'auto', ...style }}>
{icon && icon} {children} {icon && icon} {children}
{closable && <CloseIcon $size={size} $color={color} onClick={onClose} />} {closable && (
<CloseIcon
$size={size}
$color={actualColor}
onClick={(e) => {
e.stopPropagation()
onClose?.()
}}
/>
)}
</Tag> </Tag>
), ),
[children, closable, color, icon, onClose, size] [actualColor, children, closable, disabled, icon, onClick, onClose, size, style]
) )
return tooltip ? ( return tooltip ? (

View File

@ -0,0 +1,12 @@
import { useTranslation } from 'react-i18next'
import CustomTag, { CustomTagProps } from '../CustomTag'
type Props = {
size?: number
} & Omit<CustomTagProps, 'size' | 'tooltip' | 'icon' | 'color' | 'children'>
export const EmbeddingTag = ({ size, ...restProps }: Props) => {
const { t } = useTranslation()
return <CustomTag size={size} color="#FFA500" icon={t('models.type.embedding')} {...restProps} />
}

View File

@ -0,0 +1,23 @@
import { useTranslation } from 'react-i18next'
import CustomTag, { CustomTagProps } from '../CustomTag'
type Props = {
size?: number
showTooltip?: boolean
showLabel?: boolean
} & Omit<CustomTagProps, 'size' | 'tooltip' | 'icon' | 'color' | 'children'>
export const ReasoningTag = ({ size, showTooltip, showLabel, ...restProps }: Props) => {
const { t } = useTranslation()
return (
<CustomTag
size={size}
color="#6372bd"
icon={<i className="iconfont icon-thinking" />}
tooltip={showTooltip ? t('models.type.reasoning') : undefined}
{...restProps}>
{showLabel ? t('models.type.reasoning') : ''}
</CustomTag>
)
}

View File

@ -0,0 +1,12 @@
import { useTranslation } from 'react-i18next'
import CustomTag, { CustomTagProps } from '../CustomTag'
type Props = {
size?: number
} & Omit<CustomTagProps, 'size' | 'tooltip' | 'icon' | 'color' | 'children'>
export const RerankerTag = ({ size, ...restProps }: Props) => {
const { t } = useTranslation()
return <CustomTag size={size} color="#6495ED" icon={t('models.type.rerank')} {...restProps} />
}

View File

@ -0,0 +1,24 @@
import { ToolOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import CustomTag, { CustomTagProps } from '../CustomTag'
type Props = {
size?: number
showTooltip?: boolean
showLabel?: boolean
} & Omit<CustomTagProps, 'size' | 'tooltip' | 'icon' | 'color' | 'children'>
export const ToolsCallingTag = ({ size, showTooltip, showLabel, ...restProps }: Props) => {
const { t } = useTranslation()
return (
<CustomTag
size={size}
color="#f18737"
icon={<ToolOutlined style={{ fontSize: size }} />}
tooltip={showTooltip ? t('models.type.function_calling') : undefined}
{...restProps}>
{showLabel ? t('models.type.function_calling') : ''}
</CustomTag>
)
}

View File

@ -0,0 +1,25 @@
import { EyeOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import CustomTag, { CustomTagProps } from '../CustomTag'
type Props = {
size?: number
showTooltip?: boolean
showLabel?: boolean
} & Omit<CustomTagProps, 'size' | 'tooltip' | 'icon' | 'color' | 'children'>
export const VisionTag = ({ size, showTooltip, showLabel, ...restProps }: Props) => {
const { t } = useTranslation()
return (
<CustomTag
size={size}
color="#00b96b"
icon={<EyeOutlined style={{ fontSize: size }} />}
tooltip={showTooltip ? t('models.type.vision') : undefined}
{...restProps}>
{showLabel ? t('models.type.vision') : ''}
</CustomTag>
)
}

View File

@ -0,0 +1,25 @@
import { GlobalOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import CustomTag, { CustomTagProps } from '../CustomTag'
type Props = {
size?: number
showTooltip?: boolean
showLabel?: boolean
} & Omit<CustomTagProps, 'size' | 'tooltip' | 'icon' | 'color' | 'children'>
export const WebSearchTag = ({ size, showTooltip, showLabel, ...restProps }: Props) => {
const { t } = useTranslation()
return (
<CustomTag
size={size}
color="#1677ff"
icon={<GlobalOutlined style={{ fontSize: size }} />}
tooltip={showTooltip ? t('models.type.websearch') : undefined}
{...restProps}>
{showLabel ? t('models.type.websearch') : ''}
</CustomTag>
)
}

View File

@ -0,0 +1,8 @@
import { EmbeddingTag } from './EmbeddingTag'
import { ReasoningTag } from './ReasoningTag'
import { RerankerTag } from './RerankerTag'
import { ToolsCallingTag } from './ToolsCallingTag'
import { VisionTag } from './VisionTag'
import { WebSearchTag } from './WebSearchTag'
export { EmbeddingTag, ReasoningTag, RerankerTag, ToolsCallingTag, VisionTag, WebSearchTag }

View File

@ -0,0 +1,25 @@
import { Tooltip, TooltipProps } from 'antd'
import { AlertTriangle } from 'lucide-react'
type InheritedTooltipProps = Omit<TooltipProps, 'children'>
interface WarnTooltipProps extends InheritedTooltipProps {
iconColor?: string
iconSize?: string | number
iconStyle?: React.CSSProperties
}
const WarnTooltip = ({
iconColor = 'var(--color-status-warning)',
iconSize = 14,
iconStyle,
...rest
}: WarnTooltipProps) => {
return (
<Tooltip {...rest}>
<AlertTriangle size={iconSize} color={iconColor} style={{ ...iconStyle }} role="img" aria-label="Information" />
</Tooltip>
)
}
export default WarnTooltip

View File

@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import CustomTag from '../CustomTag' import CustomTag from '../Tags/CustomTag'
const COLOR = '#ff0000' const COLOR = '#ff0000'

View File

@ -3052,7 +3052,7 @@
"moresetting": { "moresetting": {
"check": { "check": {
"confirm": "确认勾选", "confirm": "确认勾选",
"warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!" "warn": "请慎重更改模型类型,选择错误的类型会导致模型无法正常使用"
}, },
"label": "更多设置", "label": "更多设置",
"warn": "风险警告" "warn": "风险警告"

View File

@ -1,9 +1,9 @@
import { ImportOutlined, PlusOutlined } from '@ant-design/icons' import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import CustomTag from '@renderer/components/CustomTag'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import CustomTag from '@renderer/components/Tags/CustomTag'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
import { useNavbarPosition } from '@renderer/hooks/useSettings' import { useNavbarPosition } from '@renderer/hooks/useSettings'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'

View File

@ -1,5 +1,5 @@
import CustomTag from '@renderer/components/CustomTag'
import { DeleteIcon, EditIcon } from '@renderer/components/Icons' import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import CustomTag from '@renderer/components/Tags/CustomTag'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'

View File

@ -12,7 +12,7 @@ import {
GlobalOutlined, GlobalOutlined,
LinkOutlined LinkOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/Tags/CustomTag'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import { FileMetadata } from '@renderer/types' import { FileMetadata } from '@renderer/types'
import { formatFileSize } from '@renderer/utils' import { formatFileSize } from '@renderer/utils'

View File

@ -1,5 +1,5 @@
import { FileSearchOutlined } from '@ant-design/icons' import { FileSearchOutlined } from '@ant-design/icons'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/Tags/CustomTag'
import { KnowledgeBase } from '@renderer/types' import { KnowledgeBase } from '@renderer/types'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'

View File

@ -1,4 +1,4 @@
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/Tags/CustomTag'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'

View File

@ -1,5 +1,5 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/Tags/CustomTag'
import TranslateButton from '@renderer/components/TranslateButton' import TranslateButton from '@renderer/components/TranslateButton'
import { isGenerateImageModel, isVisionModel } from '@renderer/config/models' import { isGenerateImageModel, isVisionModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'

View File

@ -1,7 +1,7 @@
import { RedoOutlined } from '@ant-design/icons' import { RedoOutlined } from '@ant-design/icons'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import CustomTag from '@renderer/components/CustomTag'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import CustomTag from '@renderer/components/Tags/CustomTag'
import { useKnowledge } from '@renderer/hooks/useKnowledge' import { useKnowledge } from '@renderer/hooks/useKnowledge'
import { NavbarIcon } from '@renderer/pages/home/ChatNavbar' import { NavbarIcon } from '@renderer/pages/home/ChatNavbar'
import { getProviderName } from '@renderer/services/ProviderService' import { getProviderName } from '@renderer/services/ProviderService'

View File

@ -1,4 +1,13 @@
import CopyIcon from '@renderer/components/Icons/CopyIcon' import CopyIcon from '@renderer/components/Icons/CopyIcon'
import {
EmbeddingTag,
ReasoningTag,
RerankerTag,
ToolsCallingTag,
VisionTag,
WebSearchTag
} from '@renderer/components/Tags/ModelCapabilities'
import WarnTooltip from '@renderer/components/WarnTooltip'
import { endpointTypeOptions } from '@renderer/config/endpointTypes' import { endpointTypeOptions } from '@renderer/config/endpointTypes'
import { import {
isEmbeddingModel, isEmbeddingModel,
@ -13,7 +22,6 @@ import { Model, ModelCapability, ModelType, Provider } from '@renderer/types'
import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils' import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils'
import { import {
Button, Button,
Checkbox,
Divider, Divider,
Flex, Flex,
Form, Form,
@ -23,11 +31,12 @@ import {
Modal, Modal,
ModalProps, ModalProps,
Select, Select,
Switch Switch,
Tooltip
} from 'antd' } from 'antd'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { ChevronDown, ChevronUp, SaveIcon } from 'lucide-react' import { ChevronDown, ChevronUp, RotateCcw, SaveIcon } from 'lucide-react'
import { FC, useEffect, useRef, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -107,22 +116,32 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
{ label: t('models.price.custom'), value: 'custom' } { label: t('models.price.custom'), value: 'custom' }
] ]
const defaultTypes = [ const defaultTypes: ModelType[] = useMemo(
...(isVisionModel(model) ? ['vision'] : []), () => [
...(isReasoningModel(model) ? ['reasoning'] : []), ...(isVisionModel(model) ? (['vision'] as const) : []),
...(isFunctionCallingModel(model) ? ['function_calling'] : []), ...(isReasoningModel(model) ? (['reasoning'] as const) : []),
...(isWebSearchModel(model) ? ['web_search'] : []), ...(isFunctionCallingModel(model) ? (['function_calling'] as const) : []),
...(isEmbeddingModel(model) ? ['embedding'] : []), ...(isWebSearchModel(model) ? (['web_search'] as const) : []),
...(isRerankModel(model) ? ['rerank'] : []) ...(isEmbeddingModel(model) ? (['embedding'] as const) : []),
] ...(isRerankModel(model) ? (['rerank'] as const) : [])
],
[model]
)
const selectedTypes: string[] = getUnion( const selectedTypes: ModelType[] = useMemo(
() =>
getUnion(
modelCapabilities?.filter((t) => t.isUserSelected).map((t) => t.type) || [], modelCapabilities?.filter((t) => t.isUserSelected).map((t) => t.type) || [],
getDifference(defaultTypes, modelCapabilities?.filter((t) => t.isUserSelected === false).map((t) => t.type) || []) getDifference(
defaultTypes,
modelCapabilities?.filter((t) => t.isUserSelected === false).map((t) => t.type) || []
)
),
[defaultTypes, modelCapabilities]
) )
// 被rerank/embedding改变的类型 // 被rerank/embedding改变的类型
const changedTypesRef = useRef<string[]>([]) // const changedTypesRef = useRef<string[]>([])
useEffect(() => { useEffect(() => {
if (showMoreSettings) { if (showMoreSettings) {
@ -151,157 +170,76 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
}, [modelCapabilities]) }, [modelCapabilities])
const ModelCapability = () => { const ModelCapability = () => {
const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
const isRerankDisabled = selectedTypes.includes('embedding') const isRerankDisabled = selectedTypes.includes('embedding')
const isEmbeddingDisabled = selectedTypes.includes('rerank') const isEmbeddingDisabled = selectedTypes.includes('rerank')
const showTypeConfirmModal = (newCapability: ModelCapability) => { const isOtherDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
const onUpdateType = selectedTypes?.find((t) => t === newCapability.type)
window.modal.confirm({
title: t('settings.moresetting.warn'),
content: t('settings.moresetting.check.warn'),
okText: t('settings.moresetting.check.confirm'),
cancelText: t('common.cancel'),
okButtonProps: { danger: true },
cancelButtonProps: { type: 'primary' },
onOk: () => {
if (onUpdateType) {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (t.type === newCapability.type) {
return { ...t, isUserSelected: true }
}
if (
((onUpdateType !== t.type && onUpdateType === 'rerank') ||
(onUpdateType === 'embedding' && onUpdateType !== t.type)) &&
t.isUserSelected !== false
) {
changedTypesRef.current.push(t.type)
return { ...t, isUserSelected: false }
}
return t
})
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
} else {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (
((newCapability.type !== t.type && newCapability.type === 'rerank') ||
(newCapability.type === 'embedding' && newCapability.type !== t.type)) &&
t.isUserSelected !== false
) {
changedTypesRef.current.push(t.type)
return { ...t, isUserSelected: false }
}
if (newCapability.type === t.type) {
return { ...t, isUserSelected: true }
}
return t
})
updatedModelCapabilities.push(newCapability as any)
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
}
},
onCancel: () => {},
centered: true
})
}
const handleTypeChange = (types: string[]) => {
setHasUserModified(true) // 标记用户已进行修改
const diff = types.length > selectedTypes.length
if (diff) {
const newCapability = getDifference(types, selectedTypes) // checkbox的特性确保了newCapability只有一个元素
showTypeConfirmModal({
type: newCapability[0] as ModelType,
isUserSelected: true
})
} else {
const disabledTypes = getDifference(selectedTypes, types)
const onUpdateType = modelCapabilities?.find((t) => t.type === disabledTypes[0])
if (onUpdateType) {
const updatedTypes = modelCapabilities?.map((t) => {
if (t.type === disabledTypes[0]) {
return { ...t, isUserSelected: false }
}
if (
((onUpdateType !== t && onUpdateType.type === 'rerank') ||
(onUpdateType.type === 'embedding' && onUpdateType !== t)) &&
t.isUserSelected === false
) {
if (changedTypesRef.current.includes(t.type)) {
return { ...t, isUserSelected: true }
}
}
return t
})
setModelCapabilities(uniqueObjectArray(updatedTypes as ModelCapability[]))
} else {
const updatedModelCapabilities = modelCapabilities?.map((t) => {
if (
(disabledTypes[0] === 'rerank' && t.type !== 'rerank') ||
(disabledTypes[0] === 'embedding' && t.type !== 'embedding' && t.isUserSelected === false)
) {
return { ...t, isUserSelected: true }
}
return t
})
updatedModelCapabilities.push({ type: disabledTypes[0] as ModelType, isUserSelected: false })
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
}
changedTypesRef.current.length = 0
}
}
const handleResetTypes = () => { const handleResetTypes = () => {
setModelCapabilities(originalModelCapabilities) setModelCapabilities(originalModelCapabilities)
setHasUserModified(false) // 重置后清除修改标志 setHasUserModified(false) // 重置后清除修改标志
} }
const updateType = useCallback((type: ModelType) => {
setHasUserModified(true)
setModelCapabilities((prev) =>
uniqueObjectArray([
...prev.filter((t) => t.type !== type),
{ type, isUserSelected: !selectedTypes.includes(type) }
])
)
}, [])
return ( return (
<div> <>
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}> <TypeTitle>
<Checkbox.Group <Flex align="center" gap={4} style={{ height: 24 }}>
value={selectedTypes} {t('models.type.select')}
onChange={handleTypeChange} <WarnTooltip title={t('settings.moresetting.check.warn')} />
options={[
{
label: t('models.type.vision'),
value: 'vision',
disabled: isDisabled
},
{
label: t('models.type.websearch'),
value: 'web_search',
disabled: isDisabled
},
{
label: t('models.type.rerank'),
value: 'rerank',
disabled: isRerankDisabled
},
{
label: t('models.type.embedding'),
value: 'embedding',
disabled: isEmbeddingDisabled
},
{
label: t('models.type.reasoning'),
value: 'reasoning',
disabled: isDisabled
},
{
label: t('models.type.function_calling'),
value: 'function_calling',
disabled: isDisabled
}
]}
/>
{hasUserModified && (
<Button size="small" onClick={handleResetTypes}>
{t('common.reset')}
</Button>
)}
</Flex> </Flex>
</div>
{hasUserModified && (
<Tooltip title={t('common.reset')}>
<Button size="small" icon={<RotateCcw size={14} />} onClick={handleResetTypes} type="text" />
</Tooltip>
)}
</TypeTitle>
<Flex justify="flex-start" align="center" gap={4} wrap={'wrap'} style={{ marginBottom: 8 }}>
<VisionTag
showLabel
inactive={isOtherDisabled || !selectedTypes.includes('vision')}
disabled={isOtherDisabled}
onClick={() => updateType('vision')}
/>
<WebSearchTag
showLabel
inactive={isOtherDisabled || !selectedTypes.includes('web_search')}
disabled={isOtherDisabled}
onClick={() => updateType('web_search')}
/>
<ReasoningTag
showLabel
inactive={isOtherDisabled || !selectedTypes.includes('reasoning')}
disabled={isOtherDisabled}
onClick={() => updateType('reasoning')}
/>
<ToolsCallingTag
showLabel
inactive={isOtherDisabled || !selectedTypes.includes('function_calling')}
disabled={isOtherDisabled}
onClick={() => updateType('function_calling')}
/>
<RerankerTag
disabled={isRerankDisabled}
inactive={isRerankDisabled || !selectedTypes.includes('rerank')}
onClick={() => updateType('rerank')}
/>
<EmbeddingTag
inactive={isEmbeddingDisabled || !selectedTypes.includes('embedding')}
disabled={isEmbeddingDisabled}
onClick={() => updateType('embedding')}
/>
</Flex>
</>
) )
} }
@ -405,7 +343,6 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
{showMoreSettings && ( {showMoreSettings && (
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Divider style={{ margin: '16px 0 16px 0' }} /> <Divider style={{ margin: '16px 0 16px 0' }} />
<TypeTitle>{t('models.type.select')}</TypeTitle>
<ModelCapability /> <ModelCapability />
<Divider style={{ margin: '16px 0 12px 0' }} /> <Divider style={{ margin: '16px 0 12px 0' }} />
<Form.Item <Form.Item
@ -516,6 +453,9 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
} }
const TypeTitle = styled.div` const TypeTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin: 12px 0; margin: 12px 0;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;

View File

@ -1,6 +1,6 @@
import CustomTag from '@renderer/components/CustomTag'
import ExpandableText from '@renderer/components/ExpandableText' import ExpandableText from '@renderer/components/ExpandableText'
import ModelIdWithTags from '@renderer/components/ModelIdWithTags' import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
import CustomTag from '@renderer/components/Tags/CustomTag'
import { DynamicVirtualList } from '@renderer/components/VirtualList' import { DynamicVirtualList } from '@renderer/components/VirtualList'
import { getModelLogo } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
import FileItem from '@renderer/pages/files/FileItem' import FileItem from '@renderer/pages/files/FileItem'

View File

@ -1,7 +1,7 @@
import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar' import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
import CustomTag from '@renderer/components/CustomTag'
import { LoadingIcon, StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons' import { LoadingIcon, StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import CustomTag from '@renderer/components/Tags/CustomTag'
import { PROVIDER_URLS } from '@renderer/config/providers' import { PROVIDER_URLS } from '@renderer/config/providers'
import { useProvider } from '@renderer/hooks/useProvider' import { useProvider } from '@renderer/hooks/useProvider'
import { getProviderLabel } from '@renderer/i18n/label' import { getProviderLabel } from '@renderer/i18n/label'