mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-21 16:01:35 +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}
|
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)
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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 ? (
|
||||||
@ -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 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'
|
||||||
|
|
||||||
|
|||||||
@ -3052,7 +3052,7 @@
|
|||||||
"moresetting": {
|
"moresetting": {
|
||||||
"check": {
|
"check": {
|
||||||
"confirm": "确认勾选",
|
"confirm": "确认勾选",
|
||||||
"warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!!"
|
"warn": "请慎重更改模型类型,选择错误的类型会导致模型无法正常使用!"
|
||||||
},
|
},
|
||||||
"label": "更多设置",
|
"label": "更多设置",
|
||||||
"warn": "风险警告"
|
"warn": "风险警告"
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user