mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 15:10:59 +08:00
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:
parent
ad0c2a11f3
commit
201fcf9f45
@ -78,7 +78,7 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||
style={collapseStyle}
|
||||
defaultActiveKey={defaultActiveKey}
|
||||
activeKey={activeKey}
|
||||
destroyInactivePanel={destroyInactivePanel}
|
||||
destroyOnHidden={destroyInactivePanel}
|
||||
collapsible={collapsible}
|
||||
onChange={(keys) => {
|
||||
setActiveKeys(keys)
|
||||
|
||||
@ -26,7 +26,7 @@ const ModelIdWithTags = ({
|
||||
maxWidth: '500px'
|
||||
}
|
||||
}}
|
||||
destroyTooltipOnHide
|
||||
destroyOnHidden
|
||||
title={
|
||||
<Typography.Text style={{ color: 'white' }} copyable={{ text: model.id }}>
|
||||
{model.id}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { EyeOutlined, GlobalOutlined, ToolOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
isEmbeddingModel,
|
||||
isFunctionCallingModel,
|
||||
@ -14,7 +13,15 @@ import { FC, memo, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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 {
|
||||
model: Model
|
||||
@ -70,45 +77,17 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
|
||||
|
||||
return (
|
||||
<Container ref={containerRef} style={style}>
|
||||
{isVisionModel(model) && (
|
||||
<CustomTag
|
||||
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>
|
||||
)}
|
||||
{isVisionModel(model) && <VisionTag size={size} showTooltip={showTooltip} showLabel={shouldShowLabel} />}
|
||||
{isWebSearchModel(model) && <WebSearchTag size={size} showTooltip={showTooltip} showLabel={shouldShowLabel} />}
|
||||
{showReasoning && isReasoningModel(model) && (
|
||||
<CustomTag
|
||||
size={size}
|
||||
color="#6372bd"
|
||||
icon={<i className="iconfont icon-thinking" />}
|
||||
tooltip={showTooltip ? t('models.type.reasoning') : undefined}>
|
||||
{shouldShowLabel ? t('models.type.reasoning') : ''}
|
||||
</CustomTag>
|
||||
<ReasoningTag size={size} showTooltip={showTooltip} showLabel={shouldShowLabel} />
|
||||
)}
|
||||
{showToolsCalling && isFunctionCallingModel(model) && (
|
||||
<CustomTag
|
||||
size={size}
|
||||
color="#f18737"
|
||||
icon={<ToolOutlined style={{ fontSize: size }} />}
|
||||
tooltip={showTooltip ? t('models.type.function_calling') : undefined}>
|
||||
{shouldShowLabel ? t('models.type.function_calling') : ''}
|
||||
</CustomTag>
|
||||
<ToolsCallingTag size={size} showTooltip={showTooltip} showLabel={shouldShowLabel} />
|
||||
)}
|
||||
{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')} />}
|
||||
{isRerankModel(model) && <CustomTag size={size} color="#6495ED" icon={t('models.type.rerank')} />}
|
||||
{isRerankModel(model) && <RerankerTag size={size} />}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useKnowledge, useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||
import { Message } from '@renderer/types/newMessage'
|
||||
|
||||
@ -1,27 +1,58 @@
|
||||
import { CloseOutlined } from '@ant-design/icons'
|
||||
import { Tooltip } from 'antd'
|
||||
import { FC, memo, useMemo } from 'react'
|
||||
import { CSSProperties, FC, memo, useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface CustomTagProps {
|
||||
export interface CustomTagProps {
|
||||
icon?: React.ReactNode
|
||||
children?: React.ReactNode | string
|
||||
color: string
|
||||
size?: number
|
||||
style?: CSSProperties
|
||||
tooltip?: string
|
||||
closable?: boolean
|
||||
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(
|
||||
() => (
|
||||
<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}
|
||||
{closable && <CloseIcon $size={size} $color={color} onClick={onClose} />}
|
||||
{closable && (
|
||||
<CloseIcon
|
||||
$size={size}
|
||||
$color={actualColor}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClose?.()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Tag>
|
||||
),
|
||||
[children, closable, color, icon, onClose, size]
|
||||
[actualColor, children, closable, disabled, icon, onClick, onClose, size, style]
|
||||
)
|
||||
|
||||
return tooltip ? (
|
||||
@ -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} />
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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} />
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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 }
|
||||
25
src/renderer/src/components/WarnTooltip.tsx
Normal file
25
src/renderer/src/components/WarnTooltip.tsx
Normal 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
|
||||
@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import CustomTag from '../CustomTag'
|
||||
import CustomTag from '../Tags/CustomTag'
|
||||
|
||||
const COLOR = '#ff0000'
|
||||
|
||||
|
||||
@ -3052,7 +3052,7 @@
|
||||
"moresetting": {
|
||||
"check": {
|
||||
"confirm": "确认勾选",
|
||||
"warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!!"
|
||||
"warn": "请慎重更改模型类型,选择错误的类型会导致模型无法正常使用!"
|
||||
},
|
||||
"label": "更多设置",
|
||||
"warn": "风险警告"
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useSettings'
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
GlobalOutlined,
|
||||
LinkOutlined
|
||||
} from '@ant-design/icons'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { FileMetadata } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isGenerateImageModel, isVisionModel } from '@renderer/config/models'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { RedoOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import { NavbarIcon } from '@renderer/pages/home/ChatNavbar'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
|
||||
@ -1,4 +1,13 @@
|
||||
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 {
|
||||
isEmbeddingModel,
|
||||
@ -13,7 +22,6 @@ import { Model, ModelCapability, ModelType, Provider } from '@renderer/types'
|
||||
import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Flex,
|
||||
Form,
|
||||
@ -23,11 +31,12 @@ import {
|
||||
Modal,
|
||||
ModalProps,
|
||||
Select,
|
||||
Switch
|
||||
Switch,
|
||||
Tooltip
|
||||
} from 'antd'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { ChevronDown, ChevronUp, SaveIcon } from 'lucide-react'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { ChevronDown, ChevronUp, RotateCcw, SaveIcon } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -107,22 +116,32 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
|
||||
{ label: t('models.price.custom'), value: 'custom' }
|
||||
]
|
||||
|
||||
const defaultTypes = [
|
||||
...(isVisionModel(model) ? ['vision'] : []),
|
||||
...(isReasoningModel(model) ? ['reasoning'] : []),
|
||||
...(isFunctionCallingModel(model) ? ['function_calling'] : []),
|
||||
...(isWebSearchModel(model) ? ['web_search'] : []),
|
||||
...(isEmbeddingModel(model) ? ['embedding'] : []),
|
||||
...(isRerankModel(model) ? ['rerank'] : [])
|
||||
]
|
||||
const defaultTypes: ModelType[] = useMemo(
|
||||
() => [
|
||||
...(isVisionModel(model) ? (['vision'] as const) : []),
|
||||
...(isReasoningModel(model) ? (['reasoning'] as const) : []),
|
||||
...(isFunctionCallingModel(model) ? (['function_calling'] as const) : []),
|
||||
...(isWebSearchModel(model) ? (['web_search'] as const) : []),
|
||||
...(isEmbeddingModel(model) ? (['embedding'] as const) : []),
|
||||
...(isRerankModel(model) ? (['rerank'] as const) : [])
|
||||
],
|
||||
[model]
|
||||
)
|
||||
|
||||
const selectedTypes: string[] = getUnion(
|
||||
modelCapabilities?.filter((t) => t.isUserSelected).map((t) => t.type) || [],
|
||||
getDifference(defaultTypes, modelCapabilities?.filter((t) => t.isUserSelected === false).map((t) => t.type) || [])
|
||||
const selectedTypes: ModelType[] = useMemo(
|
||||
() =>
|
||||
getUnion(
|
||||
modelCapabilities?.filter((t) => t.isUserSelected).map((t) => t.type) || [],
|
||||
getDifference(
|
||||
defaultTypes,
|
||||
modelCapabilities?.filter((t) => t.isUserSelected === false).map((t) => t.type) || []
|
||||
)
|
||||
),
|
||||
[defaultTypes, modelCapabilities]
|
||||
)
|
||||
|
||||
// 被rerank/embedding改变的类型
|
||||
const changedTypesRef = useRef<string[]>([])
|
||||
// const changedTypesRef = useRef<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (showMoreSettings) {
|
||||
@ -151,157 +170,76 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
|
||||
}, [modelCapabilities])
|
||||
|
||||
const ModelCapability = () => {
|
||||
const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
|
||||
|
||||
const isRerankDisabled = selectedTypes.includes('embedding')
|
||||
const isEmbeddingDisabled = selectedTypes.includes('rerank')
|
||||
const showTypeConfirmModal = (newCapability: ModelCapability) => {
|
||||
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 isOtherDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
|
||||
|
||||
const handleResetTypes = () => {
|
||||
setModelCapabilities(originalModelCapabilities)
|
||||
setHasUserModified(false) // 重置后清除修改标志
|
||||
}
|
||||
|
||||
const updateType = useCallback((type: ModelType) => {
|
||||
setHasUserModified(true)
|
||||
setModelCapabilities((prev) =>
|
||||
uniqueObjectArray([
|
||||
...prev.filter((t) => t.type !== type),
|
||||
{ type, isUserSelected: !selectedTypes.includes(type) }
|
||||
])
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
|
||||
<Checkbox.Group
|
||||
value={selectedTypes}
|
||||
onChange={handleTypeChange}
|
||||
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
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<>
|
||||
<TypeTitle>
|
||||
<Flex align="center" gap={4} style={{ height: 24 }}>
|
||||
{t('models.type.select')}
|
||||
<WarnTooltip title={t('settings.moresetting.check.warn')} />
|
||||
</Flex>
|
||||
|
||||
{hasUserModified && (
|
||||
<Button size="small" onClick={handleResetTypes}>
|
||||
{t('common.reset')}
|
||||
</Button>
|
||||
<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>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -405,7 +343,6 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
|
||||
{showMoreSettings && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Divider style={{ margin: '16px 0 16px 0' }} />
|
||||
<TypeTitle>{t('models.type.select')}</TypeTitle>
|
||||
<ModelCapability />
|
||||
<Divider style={{ margin: '16px 0 12px 0' }} />
|
||||
<Form.Item
|
||||
@ -516,6 +453,9 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
|
||||
}
|
||||
|
||||
const TypeTitle = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import ExpandableText from '@renderer/components/ExpandableText'
|
||||
import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import { LoadingIcon, StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { PROVIDER_URLS } from '@renderer/config/providers'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user