style(ProviderSettings): Refactor ProviderSettings UI (#4475)
* chore(version): 1.1.19 * style(ProviderSettings): Refactor ProviderSettings UI * style(CustomTag, ModelTagsWithLabel): enhance layout and styling for better UI consistency * refactor(CustomTag, ModelTagsWithLabel, MentionModelsButton): update props handling and improve component usage * feat(CustomTag, ModelTagsWithLabel): add tooltip support and improve label visibility based on container size * fix(ModelTagsWithLabel): adjust maxWidth for non-Chinese languages to improve layout * style(ModelList): add text overflow handling for list item names * feat(ModelList): enhance group label with item count using CustomTag * feat(FileItem): add style prop for customizable background color in FileItem component * style(index.scss): update border color variables for improved UI consistency * style(EditModelsPopup): update background color for model items to enhance visual distinction * style(HealthCheckPopup): update button size for improved usability * feat(CustomCollapse): add collapsible prop to customize collapse behavior * chore: remove hover models color --------- Co-authored-by: kangfenmao <kangfenmao@qq.com> Co-authored-by: eeee0717 <chentao020717Work@outlook.com>
This commit is contained in:
parent
037027f1f4
commit
3aaa1848f0
@ -36,7 +36,7 @@
|
|||||||
--color-text: var(--color-text-1);
|
--color-text: var(--color-text-1);
|
||||||
--color-icon: #ffffff99;
|
--color-icon: #ffffff99;
|
||||||
--color-icon-white: #ffffff;
|
--color-icon-white: #ffffff;
|
||||||
--color-border: #ffffff15;
|
--color-border: #ffffff19;
|
||||||
--color-border-soft: #ffffff10;
|
--color-border-soft: #ffffff10;
|
||||||
--color-border-mute: #ffffff05;
|
--color-border-mute: #ffffff05;
|
||||||
--color-error: #f44336;
|
--color-error: #f44336;
|
||||||
@ -80,7 +80,7 @@ body {
|
|||||||
|
|
||||||
body[theme-mode='light'] {
|
body[theme-mode='light'] {
|
||||||
--color-white: #ffffff;
|
--color-white: #ffffff;
|
||||||
--color-white-soft: #f2f2f2;
|
--color-white-soft: rgba(0, 0, 0, 0.04);
|
||||||
--color-white-mute: #eee;
|
--color-white-mute: #eee;
|
||||||
|
|
||||||
--color-black: #1b1b1f;
|
--color-black: #1b1b1f;
|
||||||
@ -108,7 +108,7 @@ body[theme-mode='light'] {
|
|||||||
--color-text: var(--color-text-1);
|
--color-text: var(--color-text-1);
|
||||||
--color-icon: #00000099;
|
--color-icon: #00000099;
|
||||||
--color-icon-white: #000000;
|
--color-icon-white: #000000;
|
||||||
--color-border: #00000015;
|
--color-border: #00000019;
|
||||||
--color-border-soft: #00000010;
|
--color-border-soft: #00000010;
|
||||||
--color-border-mute: #00000005;
|
--color-border-mute: #00000005;
|
||||||
--color-error: #f44336;
|
--color-error: #f44336;
|
||||||
|
|||||||
@ -5,9 +5,19 @@ interface CustomCollapseProps {
|
|||||||
label: React.ReactNode
|
label: React.ReactNode
|
||||||
extra: React.ReactNode
|
extra: React.ReactNode
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
destroyInactivePanel?: boolean
|
||||||
|
defaultActiveKey?: string[]
|
||||||
|
collapsible?: 'header' | 'icon' | 'disabled'
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomCollapse: FC<CustomCollapseProps> = ({ label, extra, children }) => {
|
const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||||
|
label,
|
||||||
|
extra,
|
||||||
|
children,
|
||||||
|
destroyInactivePanel = false,
|
||||||
|
defaultActiveKey = ['1'],
|
||||||
|
collapsible = undefined
|
||||||
|
}) => {
|
||||||
const CollapseStyle = {
|
const CollapseStyle = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
@ -27,7 +37,9 @@ const CustomCollapse: FC<CustomCollapseProps> = ({ label, extra, children }) =>
|
|||||||
<Collapse
|
<Collapse
|
||||||
bordered={false}
|
bordered={false}
|
||||||
style={CollapseStyle}
|
style={CollapseStyle}
|
||||||
defaultActiveKey={['1']}
|
defaultActiveKey={defaultActiveKey}
|
||||||
|
destroyInactivePanel={destroyInactivePanel}
|
||||||
|
collapsible={collapsible}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
styles: CollapseItemStyles,
|
styles: CollapseItemStyles,
|
||||||
|
|||||||
40
src/renderer/src/components/CustomTag.tsx
Normal file
40
src/renderer/src/components/CustomTag.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface CustomTagProps {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
children?: React.ReactNode | string
|
||||||
|
color: string
|
||||||
|
size?: number
|
||||||
|
tooltip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomTag: FC<CustomTagProps> = ({ children, icon, color, size = 12, tooltip }) => {
|
||||||
|
return (
|
||||||
|
<Tooltip title={tooltip} placement="top">
|
||||||
|
<Tag $color={color} $size={size}>
|
||||||
|
{icon && icon} {children}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomTag
|
||||||
|
|
||||||
|
const Tag = styled.div<{ $color: string; $size: number }>`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: ${({ $size }) => $size / 3}px ${({ $size }) => $size * 0.8}px;
|
||||||
|
border-radius: 99px;
|
||||||
|
color: ${({ $color }) => $color};
|
||||||
|
background-color: ${({ $color }) => $color + '20'};
|
||||||
|
font-size: ${({ $size }) => $size}px;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
.iconfont {
|
||||||
|
font-size: ${({ $size }) => $size}px;
|
||||||
|
color: ${({ $color }) => $color};
|
||||||
|
}
|
||||||
|
`
|
||||||
13
src/renderer/src/components/Icons/SVGIcon.tsx
Normal file
13
src/renderer/src/components/Icons/SVGIcon.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { SVGProps } from 'react'
|
||||||
|
|
||||||
|
export const StreamlineGoodHealthAndWellBeing = (props: SVGProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 14 14" {...props}>
|
||||||
|
{/* Icon from Streamline by Streamline - https://creativecommons.org/licenses/by/4.0/ */}
|
||||||
|
<g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="m10.097 12.468l-2.773-2.52c-1.53-1.522.717-4.423 2.773-2.045c2.104-2.33 4.303.57 2.773 2.045z"></path>
|
||||||
|
<path d="M.621 6.088h1.367l1.823 3.19l4.101-7.747l1.823 3.646"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
113
src/renderer/src/components/ModelTagsWithLabel.tsx
Normal file
113
src/renderer/src/components/ModelTagsWithLabel.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { EyeOutlined, GlobalOutlined, ToolOutlined } from '@ant-design/icons'
|
||||||
|
import {
|
||||||
|
isEmbeddingModel,
|
||||||
|
isFunctionCallingModel,
|
||||||
|
isReasoningModel,
|
||||||
|
isRerankModel,
|
||||||
|
isVisionModel,
|
||||||
|
isWebSearchModel
|
||||||
|
} from '@renderer/config/models'
|
||||||
|
import i18n from '@renderer/i18n'
|
||||||
|
import { Model } from '@renderer/types'
|
||||||
|
import { isFreeModel } from '@renderer/utils'
|
||||||
|
import { FC, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import CustomTag from './CustomTag'
|
||||||
|
|
||||||
|
interface ModelTagsProps {
|
||||||
|
model: Model
|
||||||
|
showFree?: boolean
|
||||||
|
showReasoning?: boolean
|
||||||
|
showToolsCalling?: boolean
|
||||||
|
size?: number
|
||||||
|
showLabel?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModelTagsWithLabel: FC<ModelTagsProps> = ({
|
||||||
|
model,
|
||||||
|
showFree = true,
|
||||||
|
showReasoning = true,
|
||||||
|
showToolsCalling = true,
|
||||||
|
size = 12,
|
||||||
|
showLabel = true
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [_showLabel, _setShowLabel] = useState(showLabel)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const resizeObserver = useRef<ResizeObserver>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showLabel) return
|
||||||
|
|
||||||
|
if (containerRef.current) {
|
||||||
|
const currentElement = containerRef.current
|
||||||
|
resizeObserver.current = new ResizeObserver((entries) => {
|
||||||
|
const maxWidth = i18n.language.startsWith('zh') ? 300 : 350
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const { width } = entry.contentRect
|
||||||
|
_setShowLabel(width >= maxWidth)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
resizeObserver.current.observe(currentElement)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (resizeObserver.current) {
|
||||||
|
resizeObserver.current.unobserve(currentElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}, [showLabel])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container ref={containerRef}>
|
||||||
|
{isVisionModel(model) && (
|
||||||
|
<CustomTag size={size} color="#00b96b" icon={<EyeOutlined />} tooltip={t('models.type.vision')}>
|
||||||
|
{_showLabel ? t('models.type.vision') : ''}
|
||||||
|
</CustomTag>
|
||||||
|
)}
|
||||||
|
{isWebSearchModel(model) && (
|
||||||
|
<CustomTag size={size} color="#1677ff" icon={<GlobalOutlined />} tooltip={t('models.type.websearch')}>
|
||||||
|
{_showLabel ? t('models.type.websearch') : ''}
|
||||||
|
</CustomTag>
|
||||||
|
)}
|
||||||
|
{showReasoning && isReasoningModel(model) && (
|
||||||
|
<CustomTag
|
||||||
|
size={size}
|
||||||
|
color="#6372bd"
|
||||||
|
icon={<i className="iconfont icon-thinking" />}
|
||||||
|
tooltip={t('models.type.reasoning')}>
|
||||||
|
{_showLabel ? t('models.type.reasoning') : ''}
|
||||||
|
</CustomTag>
|
||||||
|
)}
|
||||||
|
{showToolsCalling && isFunctionCallingModel(model) && (
|
||||||
|
<CustomTag size={size} color="#d45ea3" icon={<ToolOutlined />} tooltip={t('models.function_calling')}>
|
||||||
|
{_showLabel ? t('models.function_calling') : ''}
|
||||||
|
</CustomTag>
|
||||||
|
)}
|
||||||
|
{isEmbeddingModel(model) && (
|
||||||
|
<CustomTag size={size} color="#FFA500" icon={t('models.type.embedding')} tooltip={t('models.type.embedding')} />
|
||||||
|
)}
|
||||||
|
{showFree && isFreeModel(model) && (
|
||||||
|
<CustomTag size={size} color="#7cb305" icon={t('models.type.free')} tooltip={t('models.type.free')} />
|
||||||
|
)}
|
||||||
|
{isRerankModel(model) && (
|
||||||
|
<CustomTag size={size} color="#6495ED" icon={t('models.type.rerank')} tooltip={t('models.type.rerank')} />
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default ModelTagsWithLabel
|
||||||
@ -18,11 +18,13 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
interface FileItemProps {
|
interface FileItemProps {
|
||||||
fileInfo: {
|
fileInfo: {
|
||||||
|
icon?: React.ReactNode
|
||||||
name: React.ReactNode | string
|
name: React.ReactNode | string
|
||||||
ext: string
|
ext: string
|
||||||
extra?: React.ReactNode | string
|
extra?: React.ReactNode | string
|
||||||
actions: React.ReactNode
|
actions: React.ReactNode
|
||||||
}
|
}
|
||||||
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFileIcon = (type?: string) => {
|
const getFileIcon = (type?: string) => {
|
||||||
@ -73,18 +75,18 @@ const getFileIcon = (type?: string) => {
|
|||||||
return <FileUnknownFilled />
|
return <FileUnknownFilled />
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileItem: React.FC<FileItemProps> = ({ fileInfo }) => {
|
const FileItem: React.FC<FileItemProps> = ({ fileInfo, style }) => {
|
||||||
const { name, ext, extra, actions } = fileInfo
|
const { name, ext, extra, actions, icon } = fileInfo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileItemCard>
|
<FileItemCard style={style}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<FileIcon>{getFileIcon(ext)}</FileIcon>
|
<FileIcon>{icon || getFileIcon(ext)}</FileIcon>
|
||||||
<Flex vertical gap={0} flex={1} style={{ width: '0px' }}>
|
<Flex vertical justify="center" gap={0} flex={1} style={{ width: '0px' }}>
|
||||||
<FileName>{name}</FileName>
|
<FileName>{name}</FileName>
|
||||||
{extra && <FileInfo>{extra}</FileInfo>}
|
{extra && <FileInfo>{extra}</FileInfo>}
|
||||||
</Flex>
|
</Flex>
|
||||||
{actions}
|
<FileActions>{actions}</FileActions>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</FileItemCard>
|
</FileItemCard>
|
||||||
)
|
)
|
||||||
@ -96,7 +98,9 @@ const FileItemCard = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 0.5px solid var(--color-border);
|
border: 0.5px solid var(--color-border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: box-shadow 0.2s ease;
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
background-color 0.2s ease;
|
||||||
--shadow-color: rgba(0, 0, 0, 0.05);
|
--shadow-color: rgba(0, 0, 0, 0.05);
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@ -109,15 +113,19 @@ const FileItemCard = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const CardContent = styled.div`
|
const CardContent = styled.div`
|
||||||
padding: 8px 16px;
|
padding: 8px 8px 8px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const FileIcon = styled.div`
|
const FileIcon = styled.div`
|
||||||
|
max-height: 44px;
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
const FileName = styled.div`
|
const FileName = styled.div`
|
||||||
@ -140,4 +148,11 @@ const FileInfo = styled.div`
|
|||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const FileActions = styled.div`
|
||||||
|
max-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
|
||||||
export default memo(FileItem)
|
export default memo(FileItem)
|
||||||
|
|||||||
@ -66,7 +66,7 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
|
|||||||
<VirtualList
|
<VirtualList
|
||||||
data={list}
|
data={list}
|
||||||
height={window.innerHeight - 100}
|
height={window.innerHeight - 100}
|
||||||
itemHeight={80}
|
itemHeight={75}
|
||||||
itemKey="key"
|
itemKey="key"
|
||||||
style={{ padding: '0 16px 16px 16px' }}
|
style={{ padding: '0 16px 16px 16px' }}
|
||||||
styles={{
|
styles={{
|
||||||
@ -80,7 +80,7 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
|
|||||||
{(item) => (
|
{(item) => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: '80px',
|
height: '75px',
|
||||||
paddingTop: '12px'
|
paddingTop: '12px'
|
||||||
}}>
|
}}>
|
||||||
<FileItem
|
<FileItem
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons'
|
import { PlusOutlined } from '@ant-design/icons'
|
||||||
import ModelTags from '@renderer/components/ModelTags'
|
import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel'
|
||||||
import { useQuickPanel } from '@renderer/components/QuickPanel'
|
import { useQuickPanel } from '@renderer/components/QuickPanel'
|
||||||
import { QuickPanelListItem } from '@renderer/components/QuickPanel/types'
|
import { QuickPanelListItem } from '@renderer/components/QuickPanel/types'
|
||||||
import { getModelLogo, isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
import { getModelLogo, isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||||
@ -51,7 +51,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
|||||||
.reverse()
|
.reverse()
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
label: `${item.provider.isSystem ? t(`provider.${item.provider.id}`) : item.provider.name} | ${item.model.name}`,
|
label: `${item.provider.isSystem ? t(`provider.${item.provider.id}`) : item.provider.name} | ${item.model.name}`,
|
||||||
description: <ModelTags model={item.model} />,
|
description: <ModelTagsWithLabel model={item.model} showLabel={false} size={10} />,
|
||||||
icon: (
|
icon: (
|
||||||
<Avatar src={getModelLogo(item.model.id)} size={20}>
|
<Avatar src={getModelLogo(item.model.id)} size={20}>
|
||||||
{first(item.model.name)}
|
{first(item.model.name)}
|
||||||
|
|||||||
@ -493,7 +493,6 @@ const TopicListItem = styled.div`
|
|||||||
}
|
}
|
||||||
.menu {
|
.menu {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
SettingOutlined
|
SettingOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
import CustomTag from '@renderer/components/CustomTag'
|
||||||
import Ellipsis from '@renderer/components/Ellipsis'
|
import Ellipsis from '@renderer/components/Ellipsis'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
@ -18,7 +19,7 @@ import { getProviderName } from '@renderer/services/ProviderService'
|
|||||||
import { FileType, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
import { FileType, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||||
import { formatFileSize } from '@renderer/utils'
|
import { formatFileSize } from '@renderer/utils'
|
||||||
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
|
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
|
||||||
import { Alert, Button, Dropdown, Empty, message, Tag, Tooltip, Upload } from 'antd'
|
import { Alert, Button, Dropdown, Empty, Flex, message, Tooltip, Upload } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import VirtualList from 'rc-virtual-list'
|
import VirtualList from 'rc-virtual-list'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
@ -269,8 +270,8 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
) : (
|
) : (
|
||||||
<VirtualList
|
<VirtualList
|
||||||
data={fileItems.reverse()}
|
data={fileItems.reverse()}
|
||||||
height={fileItems.length > 5 ? 400 : fileItems.length * 80}
|
height={fileItems.length > 5 ? 400 : fileItems.length * 75}
|
||||||
itemHeight={80}
|
itemHeight={75}
|
||||||
itemKey="id"
|
itemKey="id"
|
||||||
styles={{
|
styles={{
|
||||||
verticalScrollBar: {
|
verticalScrollBar: {
|
||||||
@ -283,7 +284,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
{(item) => {
|
{(item) => {
|
||||||
const file = item.content as FileType
|
const file = item.content as FileType
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '80px', paddingTop: '12px' }}>
|
<div style={{ height: '75px', paddingTop: '12px' }}>
|
||||||
<FileItem
|
<FileItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
fileInfo={{
|
fileInfo={{
|
||||||
@ -537,45 +538,55 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
))}
|
))}
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
</CustomCollapse>
|
</CustomCollapse>
|
||||||
<ModelInfo>
|
|
||||||
<div className="model-header">
|
|
||||||
<label>{t('knowledge.model_info')}</label>
|
|
||||||
<Button icon={<SettingOutlined />} onClick={() => KnowledgeSettingsPopup.show({ base })} size="small" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="model-row">
|
<CustomCollapse
|
||||||
<div className="label-column">
|
collapsible="icon"
|
||||||
<label>{t('models.embedding_model')}</label>
|
label={
|
||||||
</div>
|
<Flex gap={8} align="center">
|
||||||
<div className="tag-column">
|
{t('knowledge.model_info')}
|
||||||
{providerName && <Tag color="purple">{providerName}</Tag>}
|
<Button
|
||||||
<Tag color="blue">{base.model.name}</Tag>
|
type="text"
|
||||||
<Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag>
|
icon={<SettingOutlined />}
|
||||||
</div>
|
onClick={() => KnowledgeSettingsPopup.show({ base })}
|
||||||
</div>
|
size="small"
|
||||||
|
/>
|
||||||
{base.rerankModel && (
|
</Flex>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => KnowledgeSearchPopup.show({ base })}
|
||||||
|
icon={<SearchOutlined />}
|
||||||
|
disabled={disabled}>
|
||||||
|
{t('knowledge.search')}
|
||||||
|
</Button>
|
||||||
|
}>
|
||||||
|
<ModelInfo>
|
||||||
<div className="model-row">
|
<div className="model-row">
|
||||||
<div className="label-column">
|
<div className="label-column">
|
||||||
<label>{t('models.rerank_model')}</label>
|
<label>{t('models.embedding_model')}</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="tag-column">
|
<div className="tag-column">
|
||||||
{rerankModelProviderName && <Tag color="purple">{rerankModelProviderName}</Tag>}
|
{providerName && <CustomTag color="#af21af">{providerName}</CustomTag>}
|
||||||
<Tag color="blue">{base.rerankModel?.name}</Tag>
|
<CustomTag color="#0000ff">{base.model.name}</CustomTag>
|
||||||
|
<CustomTag color="#00b1b1">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</CustomTag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</ModelInfo>
|
|
||||||
|
|
||||||
<IndexSection>
|
{base.rerankModel && (
|
||||||
<Button
|
<div className="model-row">
|
||||||
type="primary"
|
<div className="label-column">
|
||||||
onClick={() => KnowledgeSearchPopup.show({ base })}
|
<label>{t('models.rerank_model')}</label>
|
||||||
icon={<SearchOutlined />}
|
</div>
|
||||||
disabled={disabled}>
|
<div className="tag-column">
|
||||||
{t('knowledge.search')}
|
{rerankModelProviderName && <CustomTag color="#af21af">{rerankModelProviderName}</CustomTag>}
|
||||||
</Button>
|
<CustomTag color="#0000ff">{base.rerankModel?.name}</CustomTag>
|
||||||
</IndexSection>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModelInfo>
|
||||||
|
</CustomCollapse>
|
||||||
|
|
||||||
<BottomSpacer />
|
<BottomSpacer />
|
||||||
</MainContent>
|
</MainContent>
|
||||||
@ -588,9 +599,9 @@ const CollapseLabel = ({ label, count }: { label: string; count: number }) => {
|
|||||||
return (
|
return (
|
||||||
<HStack alignItems="center" gap={10}>
|
<HStack alignItems="center" gap={10}>
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
<Tag style={{ borderRadius: 100, padding: '0 10px' }} color={count ? 'green' : 'default'}>
|
<CustomTag size={12} color={count ? '#008001' : '#cccccc'}>
|
||||||
{count}
|
{count}
|
||||||
</Tag>
|
</CustomTag>
|
||||||
</HStack>
|
</HStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -605,12 +616,6 @@ const MainContent = styled(Scrollbar)`
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const IndexSection = styled.div`
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ModelInfo = styled.div`
|
const ModelInfo = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -629,6 +634,7 @@ const ModelInfo = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-column {
|
.label-column {
|
||||||
|
|||||||
@ -120,13 +120,11 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
|||||||
tooltip={t('settings.models.add.group_name.tooltip')}>
|
tooltip={t('settings.models.add.group_name.tooltip')}>
|
||||||
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
|
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
|
||||||
<Flex justify="center" align="center" style={{ position: 'relative' }}>
|
<Flex justify="end" align="center" style={{ position: 'relative' }}>
|
||||||
<div>
|
<Button type="primary" htmlType="submit" size="middle">
|
||||||
<Button type="primary" htmlType="submit" size="middle">
|
{t('settings.models.add.add_model')}
|
||||||
{t('settings.models.add.add_model')}
|
</Button>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
import { LoadingOutlined, MinusOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'
|
||||||
import { Center } from '@renderer/components/Layout'
|
import CustomCollapse from '@renderer/components/CustomCollapse'
|
||||||
import ModelTags from '@renderer/components/ModelTags'
|
import CustomTag from '@renderer/components/CustomTag'
|
||||||
|
import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel'
|
||||||
import {
|
import {
|
||||||
getModelLogo,
|
getModelLogo,
|
||||||
isEmbeddingModel,
|
isEmbeddingModel,
|
||||||
@ -12,11 +13,12 @@ import {
|
|||||||
SYSTEM_MODELS
|
SYSTEM_MODELS
|
||||||
} from '@renderer/config/models'
|
} from '@renderer/config/models'
|
||||||
import { useProvider } from '@renderer/hooks/useProvider'
|
import { useProvider } from '@renderer/hooks/useProvider'
|
||||||
|
import FileItem from '@renderer/pages/files/FileItem'
|
||||||
import { fetchModels } from '@renderer/services/ApiService'
|
import { fetchModels } from '@renderer/services/ApiService'
|
||||||
import { Model, Provider } from '@renderer/types'
|
import { Model, Provider } from '@renderer/types'
|
||||||
import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils'
|
import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils'
|
||||||
import { Avatar, Button, Empty, Flex, Modal, Popover, Radio, Tooltip } from 'antd'
|
import { Avatar, Button, Empty, Flex, Modal, Tabs, Tooltip, Typography } from 'antd'
|
||||||
import Search from 'antd/es/input/Search'
|
import Input from 'antd/es/input/Input'
|
||||||
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -163,51 +165,63 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
width="680px"
|
width="680px"
|
||||||
styles={{
|
styles={{
|
||||||
content: { padding: 0 },
|
content: { padding: 0 },
|
||||||
header: { padding: 22, paddingBottom: 15 }
|
header: { padding: '16px 22px 30px 22px' }
|
||||||
}}
|
}}
|
||||||
centered>
|
centered>
|
||||||
<SearchContainer>
|
<SearchContainer>
|
||||||
<Center>
|
<Input
|
||||||
<Radio.Group
|
prefix={<SearchOutlined />}
|
||||||
size={i18n.language.startsWith('zh') ? 'middle' : 'small'}
|
size="large"
|
||||||
value={filterType}
|
|
||||||
onChange={(e) => setFilterType(e.target.value)}
|
|
||||||
buttonStyle="solid">
|
|
||||||
<Radio.Button value="all">{t('models.all')}</Radio.Button>
|
|
||||||
<Radio.Button value="reasoning">{t('models.type.reasoning')}</Radio.Button>
|
|
||||||
<Radio.Button value="vision">{t('models.type.vision')}</Radio.Button>
|
|
||||||
<Radio.Button value="websearch">{t('models.type.websearch')}</Radio.Button>
|
|
||||||
<Radio.Button value="free">{t('models.type.free')}</Radio.Button>
|
|
||||||
<Radio.Button value="embedding">{t('models.type.embedding')}</Radio.Button>
|
|
||||||
<Radio.Button value="rerank">{t('models.type.rerank')}</Radio.Button>
|
|
||||||
<Radio.Button value="function_calling">{t('models.type.function_calling')}</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Center>
|
|
||||||
<Search
|
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
placeholder={t('settings.provider.search_placeholder')}
|
placeholder={t('settings.provider.search_placeholder')}
|
||||||
allowClear
|
allowClear
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
onSearch={setSearchText}
|
/>
|
||||||
|
<Tabs
|
||||||
|
size={i18n.language.startsWith('zh') ? 'middle' : 'small'}
|
||||||
|
defaultActiveKey="all"
|
||||||
|
items={[
|
||||||
|
{ label: t('models.all'), key: 'all' },
|
||||||
|
{ label: t('models.type.reasoning'), key: 'reasoning' },
|
||||||
|
{ label: t('models.type.vision'), key: 'vision' },
|
||||||
|
{ label: t('models.type.websearch'), key: 'websearch' },
|
||||||
|
{ label: t('models.type.free'), key: 'free' },
|
||||||
|
{ label: t('models.type.embedding'), key: 'embedding' },
|
||||||
|
{ label: t('models.type.rerank'), key: 'rerank' },
|
||||||
|
{ label: t('models.type.function_calling'), key: 'function_calling' }
|
||||||
|
]}
|
||||||
|
onChange={(key) => setFilterType(key)}
|
||||||
/>
|
/>
|
||||||
</SearchContainer>
|
</SearchContainer>
|
||||||
<ListContainer>
|
<ListContainer>
|
||||||
{Object.keys(modelGroups).map((group) => {
|
{Object.keys(modelGroups).map((group, i) => {
|
||||||
const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id))
|
const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id))
|
||||||
return (
|
return (
|
||||||
<div key={group}>
|
<CustomCollapse
|
||||||
<ListHeader key={group}>
|
key={i}
|
||||||
{group}
|
defaultActiveKey={i >= 5 ? [] : ['1']}
|
||||||
<div>
|
label={
|
||||||
|
<Flex align="center" gap={10}>
|
||||||
|
<span>{group}</span>
|
||||||
|
<CustomTag color="#02B96B" size={10}>
|
||||||
|
{modelGroups[group].length}
|
||||||
|
</CustomTag>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Tooltip
|
||||||
|
destroyTooltipOnHide
|
||||||
|
title={
|
||||||
|
isAllInProvider
|
||||||
|
? t(`settings.models.manage.remove_whole_group`)
|
||||||
|
: t(`settings.models.manage.add_whole_group`)
|
||||||
|
}
|
||||||
|
placement="top">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
||||||
title={
|
onClick={(e) => {
|
||||||
isAllInProvider
|
e.stopPropagation()
|
||||||
? t(`settings.models.manage.remove_whole_group`)
|
|
||||||
: t(`settings.models.manage.add_whole_group`)
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
if (isAllInProvider) {
|
if (isAllInProvider) {
|
||||||
modelGroups[group]
|
modelGroups[group]
|
||||||
.filter((model) => isModelInProvider(provider, model.id))
|
.filter((model) => isModelInProvider(provider, model.id))
|
||||||
@ -217,40 +231,68 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Tooltip>
|
||||||
</ListHeader>
|
}>
|
||||||
{modelGroups[group].map((model) => {
|
<FlexColumn>
|
||||||
return (
|
{modelGroups[group].map((model) => (
|
||||||
<ListItem key={model.id}>
|
<FileItem
|
||||||
<ListItemHeader>
|
style={{
|
||||||
<Avatar src={getModelLogo(model.id)} size={24}>
|
backgroundColor: isModelInProvider(provider, model.id)
|
||||||
{model?.name?.[0]?.toUpperCase()}
|
? 'rgba(0, 126, 0, 0.06)'
|
||||||
</Avatar>
|
: 'rgba(255, 255, 255, 0.04)'
|
||||||
<ListItemName>
|
}}
|
||||||
<Tooltip title={model.id} placement="top">
|
key={model.id}
|
||||||
<span style={{ cursor: 'help' }}>{model.name}</span>
|
fileInfo={{
|
||||||
</Tooltip>
|
icon: <Avatar src={getModelLogo(model.id)}>{model?.name?.[0]?.toUpperCase()}</Avatar>,
|
||||||
<ModelTags model={model} />
|
name: (
|
||||||
{!isEmpty(model.description) && (
|
<ListItemName>
|
||||||
<Popover
|
<Tooltip
|
||||||
trigger="click"
|
styles={{
|
||||||
title={model.name}
|
root: {
|
||||||
content={model.description}
|
width: 'auto',
|
||||||
overlayStyle={{ maxWidth: 600 }}>
|
maxWidth: '500px'
|
||||||
<Question />
|
}
|
||||||
</Popover>
|
}}
|
||||||
)}
|
destroyTooltipOnHide
|
||||||
</ListItemName>
|
title={
|
||||||
</ListItemHeader>
|
<Typography.Text style={{ color: 'white' }} copyable={{ text: model.id }}>
|
||||||
{isModelInProvider(provider, model.id) ? (
|
{model.id}
|
||||||
<Button type="default" onClick={() => onRemoveModel(model)} icon={<MinusOutlined />} />
|
</Typography.Text>
|
||||||
) : (
|
}
|
||||||
<Button type="primary" onClick={() => onAddModel(model)} icon={<PlusOutlined />} />
|
placement="top">
|
||||||
)}
|
<span style={{ cursor: 'help' }}>{model.name}</span>
|
||||||
</ListItem>
|
</Tooltip>
|
||||||
)
|
</ListItemName>
|
||||||
})}
|
),
|
||||||
</div>
|
extra: (
|
||||||
|
<div style={{ marginTop: 6 }}>
|
||||||
|
<ModelTagsWithLabel model={model} size={11} />
|
||||||
|
|
||||||
|
{model.description && (
|
||||||
|
<Typography.Paragraph
|
||||||
|
type="secondary"
|
||||||
|
ellipsis={{ rows: 1, expandable: true }}
|
||||||
|
style={{ marginBottom: 0, marginTop: 5 }}>
|
||||||
|
{model.description}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
ext: '.model',
|
||||||
|
actions: (
|
||||||
|
<div>
|
||||||
|
{isModelInProvider(provider, model.id) ? (
|
||||||
|
<Button type="text" onClick={() => onRemoveModel(model)} icon={<MinusOutlined />} />
|
||||||
|
) : (
|
||||||
|
<Button type="text" onClick={() => onAddModel(model)} icon={<PlusOutlined />} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FlexColumn>
|
||||||
|
</CustomCollapse>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{isEmpty(list) && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('settings.models.empty')} />}
|
{isEmpty(list) && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('settings.models.empty')} />}
|
||||||
@ -264,7 +306,6 @@ const SearchContainer = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
padding: 0 22px;
|
padding: 0 22px;
|
||||||
padding-bottom: 10px;
|
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
|
|
||||||
.ant-radio-group {
|
.ant-radio-group {
|
||||||
@ -274,37 +315,21 @@ const SearchContainer = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const ListContainer = styled.div`
|
const ListContainer = styled.div`
|
||||||
max-height: 70vh;
|
height: calc(100vh - 300px);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding-bottom: 20px;
|
padding: 0 6px 16px 6px;
|
||||||
`
|
margin-left: 16px;
|
||||||
|
|
||||||
const ListHeader = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
padding: 8px 22px;
|
|
||||||
color: var(--color-text);
|
|
||||||
opacity: 0.4;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ListItem = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px 22px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ListItemHeader = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
height: 22px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FlexColumn = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ListItemName = styled.div`
|
const ListItemName = styled.div`
|
||||||
@ -314,8 +339,8 @@ const ListItemName = styled.div`
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-left: 6px;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const ModelHeaderTitle = styled.div`
|
const ModelHeaderTitle = styled.div`
|
||||||
@ -325,11 +350,6 @@ const ModelHeaderTitle = styled.div`
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Question = styled(QuestionCircleOutlined)`
|
|
||||||
cursor: pointer;
|
|
||||||
color: #888;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default class EditModelsPopup {
|
export default class EditModelsPopup {
|
||||||
static topviewId = 0
|
static topviewId = 0
|
||||||
static hide() {
|
static hide() {
|
||||||
|
|||||||
@ -160,7 +160,7 @@ const PopupContainer: React.FC<Props> = ({ title, apiKeys, resolve }) => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
<Button key="start" type="primary" onClick={onStart}>
|
<Button key="start" type="primary" onClick={onStart} size="small">
|
||||||
{t('settings.models.check.start')}
|
{t('settings.models.check.start')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@ -102,18 +102,14 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
|
|||||||
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
|
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
|
||||||
<Flex justify="center" align="center" style={{ position: 'relative' }}>
|
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
|
||||||
<div>
|
<MoreSettingsRow onClick={() => setShowModelTypes(!showModelTypes)}>
|
||||||
<Button type="primary" htmlType="submit" size="middle">
|
|
||||||
{t('common.save')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<MoreSettingsRow
|
|
||||||
onClick={() => setShowModelTypes(!showModelTypes)}
|
|
||||||
style={{ position: 'absolute', right: 0 }}>
|
|
||||||
{t('settings.moresetting')}
|
{t('settings.moresetting')}
|
||||||
<ExpandIcon>{showModelTypes ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
|
<ExpandIcon>{showModelTypes ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
|
||||||
</MoreSettingsRow>
|
</MoreSettingsRow>
|
||||||
|
<Button type="primary" htmlType="submit" size="middle">
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{showModelTypes && (
|
{showModelTypes && (
|
||||||
|
|||||||
@ -5,20 +5,24 @@ import {
|
|||||||
ExclamationCircleFilled,
|
ExclamationCircleFilled,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
MinusCircleOutlined,
|
MinusCircleOutlined,
|
||||||
|
MinusOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SettingOutlined
|
SettingOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import ModelTags from '@renderer/components/ModelTags'
|
import CustomCollapse from '@renderer/components/CustomCollapse'
|
||||||
|
import CustomTag from '@renderer/components/CustomTag'
|
||||||
|
import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel'
|
||||||
import { getModelLogo } from '@renderer/config/models'
|
import { getModelLogo } from '@renderer/config/models'
|
||||||
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
||||||
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
|
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
import { useProvider } from '@renderer/hooks/useProvider'
|
import { useProvider } from '@renderer/hooks/useProvider'
|
||||||
|
import FileItem from '@renderer/pages/files/FileItem'
|
||||||
import { ModelCheckStatus } from '@renderer/services/HealthCheckService'
|
import { ModelCheckStatus } from '@renderer/services/HealthCheckService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setModel } from '@renderer/store/assistants'
|
import { setModel } from '@renderer/store/assistants'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { maskApiKey } from '@renderer/utils/api'
|
import { maskApiKey } from '@renderer/utils/api'
|
||||||
import { Avatar, Button, Card, Flex, Space, Tooltip, Typography } from 'antd'
|
import { Avatar, Button, Flex, Tooltip, Typography } from 'antd'
|
||||||
import { groupBy, sortBy, toPairs } from 'lodash'
|
import { groupBy, sortBy, toPairs } from 'lodash'
|
||||||
import React, { memo, useCallback, useMemo, useState } from 'react'
|
import React, { memo, useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -240,71 +244,107 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Object.keys(sortedModelGroups).map((group) => (
|
<Flex gap={12} vertical>
|
||||||
<Card
|
{Object.keys(sortedModelGroups).map((group, i) => (
|
||||||
key={group}
|
<CustomCollapse
|
||||||
type="inner"
|
defaultActiveKey={i <= 5 ? ['1'] : []}
|
||||||
title={group}
|
key={group}
|
||||||
extra={
|
label={
|
||||||
<Tooltip title={t('settings.models.manage.remove_whole_group')}>
|
<Flex align="center" gap={10}>
|
||||||
<HoveredRemoveIcon
|
<span>{group}</span>
|
||||||
onClick={() =>
|
<CustomTag color="#02B96B" size={10}>
|
||||||
modelGroups[group]
|
{modelGroups[group].length}
|
||||||
.filter((model) => provider.models.some((m) => m.id === model.id))
|
</CustomTag>
|
||||||
.forEach((model) => removeModel(model))
|
</Flex>
|
||||||
}
|
}
|
||||||
/>
|
extra={
|
||||||
</Tooltip>
|
<Tooltip title={t('settings.models.manage.remove_whole_group')}>
|
||||||
}
|
<HoveredRemoveIcon
|
||||||
style={{ marginBottom: '10px', border: '0.5px solid var(--color-border)' }}
|
onClick={() =>
|
||||||
size="small">
|
modelGroups[group]
|
||||||
{sortedModelGroups[group].map((model) => {
|
.filter((model) => provider.models.some((m) => m.id === model.id))
|
||||||
const modelStatus = modelStatuses.find((status) => status.model.id === model.id)
|
.forEach((model) => removeModel(model))
|
||||||
const isChecking = modelStatus?.checking === true
|
}
|
||||||
console.log('model', model.id, getModelLogo(model.id))
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}>
|
||||||
|
<Flex gap={10} vertical style={{ marginTop: 10 }}>
|
||||||
|
{sortedModelGroups[group].map((model) => {
|
||||||
|
const modelStatus = modelStatuses.find((status) => status.model.id === model.id)
|
||||||
|
const isChecking = modelStatus?.checking === true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModelListItem key={model.id}>
|
<FileItem
|
||||||
<ModelListHeader>
|
key={model.id}
|
||||||
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
|
fileInfo={{
|
||||||
{model?.name?.[0]?.toUpperCase()}
|
icon: <Avatar src={getModelLogo(model.id)}>{model?.name?.[0]?.toUpperCase()}</Avatar>,
|
||||||
</Avatar>
|
name: (
|
||||||
<ModelNameRow>
|
<ListItemName>
|
||||||
<span>{model?.name}</span>
|
<Tooltip
|
||||||
<ModelTags model={model} />
|
styles={{
|
||||||
</ModelNameRow>
|
root: {
|
||||||
<SettingIcon
|
width: 'auto',
|
||||||
onClick={() => !isChecking && onEditModel(model)}
|
maxWidth: '500px'
|
||||||
style={{ cursor: isChecking ? 'not-allowed' : 'pointer', opacity: isChecking ? 0.5 : 1 }}
|
}
|
||||||
|
}}
|
||||||
|
destroyTooltipOnHide
|
||||||
|
title={
|
||||||
|
<Typography.Text style={{ color: 'white' }} copyable={{ text: model.id }}>
|
||||||
|
{model.id}
|
||||||
|
</Typography.Text>
|
||||||
|
}
|
||||||
|
placement="top">
|
||||||
|
<span style={{ cursor: 'help' }}>{model.name}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemName>
|
||||||
|
),
|
||||||
|
extra: (
|
||||||
|
<div style={{ marginTop: 6 }}>
|
||||||
|
<ModelTagsWithLabel model={model} size={11} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
ext: '.model',
|
||||||
|
actions: (
|
||||||
|
<Flex gap={4} align="center">
|
||||||
|
{renderLatencyText(modelStatus)}
|
||||||
|
{renderStatusIndicator(modelStatus)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
onClick={() => !isChecking && onEditModel(model)}
|
||||||
|
disabled={isChecking}
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
onClick={() => !isChecking && removeModel(model)}
|
||||||
|
disabled={isChecking}
|
||||||
|
icon={<MinusOutlined />}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{renderLatencyText(modelStatus)}
|
)
|
||||||
</ModelListHeader>
|
})}
|
||||||
<Space>
|
</Flex>
|
||||||
{renderStatusIndicator(modelStatus)}
|
</CustomCollapse>
|
||||||
<RemoveIcon
|
))}
|
||||||
onClick={() => !isChecking && removeModel(model)}
|
{docsWebsite && (
|
||||||
style={{ cursor: isChecking ? 'not-allowed' : 'pointer', opacity: isChecking ? 0.5 : 1 }}
|
<SettingHelpTextRow>
|
||||||
/>
|
<SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
|
||||||
</Space>
|
<SettingHelpLink target="_blank" href={docsWebsite}>
|
||||||
</ModelListItem>
|
{t(`provider.${provider.id}`) + ' '}
|
||||||
)
|
{t('common.docs')}
|
||||||
})}
|
</SettingHelpLink>
|
||||||
</Card>
|
<SettingHelpText>{t('common.and')}</SettingHelpText>
|
||||||
))}
|
<SettingHelpLink target="_blank" href={modelsWebsite}>
|
||||||
{docsWebsite && (
|
{t('common.models')}
|
||||||
<SettingHelpTextRow>
|
</SettingHelpLink>
|
||||||
<SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
|
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
|
||||||
<SettingHelpLink target="_blank" href={docsWebsite}>
|
</SettingHelpTextRow>
|
||||||
{t(`provider.${provider.id}`) + ' '}
|
)}
|
||||||
{t('common.docs')}
|
</Flex>
|
||||||
</SettingHelpLink>
|
|
||||||
<SettingHelpText>{t('common.and')}</SettingHelpText>
|
|
||||||
<SettingHelpLink target="_blank" href={modelsWebsite}>
|
|
||||||
{t('common.models')}
|
|
||||||
</SettingHelpLink>
|
|
||||||
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
|
|
||||||
</SettingHelpTextRow>
|
|
||||||
)}
|
|
||||||
<Flex gap={10} style={{ marginTop: '10px' }}>
|
<Flex gap={10} style={{ marginTop: '10px' }}>
|
||||||
<Button type="primary" onClick={onManageModel} icon={<EditOutlined />}>
|
<Button type="primary" onClick={onManageModel} icon={<EditOutlined />}>
|
||||||
{t('button.manage')}
|
{t('button.manage')}
|
||||||
@ -326,25 +366,20 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModelListItem = styled.div`
|
const ListItemName = styled.div`
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 5px 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ModelListHeader = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ModelNameRow = styled.div`
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const RemoveIcon = styled(MinusCircleOutlined)`
|
const RemoveIcon = styled(MinusCircleOutlined)`
|
||||||
@ -365,21 +400,11 @@ const HoveredRemoveIcon = styled(RemoveIcon)`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const SettingIcon = styled(SettingOutlined)`
|
|
||||||
margin-left: 2px;
|
|
||||||
color: var(--color-text);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-text-2);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const StatusIndicator = styled.div<{ type: string }>`
|
const StatusIndicator = styled.div<{ type: string }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 18px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: ${(props) => {
|
color: ${(props) => {
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
@ -398,6 +423,7 @@ const StatusIndicator = styled.div<{ type: string }>`
|
|||||||
const ModelLatencyText = styled(Typography.Text)`
|
const ModelLatencyText = styled(Typography.Text)`
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default memo(ModelList)
|
export default memo(ModelList)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { CheckOutlined, ExportOutlined, HeartOutlined, LoadingOutlined, SettingOutlined } from '@ant-design/icons'
|
import { CheckOutlined, ExportOutlined, LoadingOutlined, SettingOutlined } from '@ant-design/icons'
|
||||||
|
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
|
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
|
||||||
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||||
@ -383,7 +384,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
<SettingSubtitle style={{ marginBottom: 5 }}>
|
<SettingSubtitle style={{ marginBottom: 5 }}>
|
||||||
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
|
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<span>{t('common.models')}</span>
|
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.models')}</SettingSubtitle>
|
||||||
{!isEmpty(models) && <ModelListSearchBar onSearch={setModelSearchText} />}
|
{!isEmpty(models) && <ModelListSearchBar onSearch={setModelSearchText} />}
|
||||||
</Space>
|
</Space>
|
||||||
{!isEmpty(models) && (
|
{!isEmpty(models) && (
|
||||||
@ -391,7 +392,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<HeartOutlined />}
|
icon={<StreamlineGoodHealthAndWellBeing />}
|
||||||
onClick={onHealthCheck}
|
onClick={onHealthCheck}
|
||||||
loading={isHealthChecking}
|
loading={isHealthChecking}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
|
|||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
font-family: Ubuntu;
|
font-family: Ubuntu;
|
||||||
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
|
/* background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')}; */
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user