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:
Teo 2025-04-08 19:37:11 +08:00 committed by GitHub
parent 037027f1f4
commit 3aaa1848f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 518 additions and 279 deletions

View File

@ -36,7 +36,7 @@
--color-text: var(--color-text-1);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff15;
--color-border: #ffffff19;
--color-border-soft: #ffffff10;
--color-border-mute: #ffffff05;
--color-error: #f44336;
@ -80,7 +80,7 @@ body {
body[theme-mode='light'] {
--color-white: #ffffff;
--color-white-soft: #f2f2f2;
--color-white-soft: rgba(0, 0, 0, 0.04);
--color-white-mute: #eee;
--color-black: #1b1b1f;
@ -108,7 +108,7 @@ body[theme-mode='light'] {
--color-text: var(--color-text-1);
--color-icon: #00000099;
--color-icon-white: #000000;
--color-border: #00000015;
--color-border: #00000019;
--color-border-soft: #00000010;
--color-border-mute: #00000005;
--color-error: #f44336;

View File

@ -5,9 +5,19 @@ interface CustomCollapseProps {
label: React.ReactNode
extra: 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 = {
width: '100%',
background: 'transparent',
@ -27,7 +37,9 @@ const CustomCollapse: FC<CustomCollapseProps> = ({ label, extra, children }) =>
<Collapse
bordered={false}
style={CollapseStyle}
defaultActiveKey={['1']}
defaultActiveKey={defaultActiveKey}
destroyInactivePanel={destroyInactivePanel}
collapsible={collapsible}
items={[
{
styles: CollapseItemStyles,

View 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};
}
`

View 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>
)
}

View 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

View File

@ -18,11 +18,13 @@ import styled from 'styled-components'
interface FileItemProps {
fileInfo: {
icon?: React.ReactNode
name: React.ReactNode | string
ext: string
extra?: React.ReactNode | string
actions: React.ReactNode
}
style?: React.CSSProperties
}
const getFileIcon = (type?: string) => {
@ -73,18 +75,18 @@ const getFileIcon = (type?: string) => {
return <FileUnknownFilled />
}
const FileItem: React.FC<FileItemProps> = ({ fileInfo }) => {
const { name, ext, extra, actions } = fileInfo
const FileItem: React.FC<FileItemProps> = ({ fileInfo, style }) => {
const { name, ext, extra, actions, icon } = fileInfo
return (
<FileItemCard>
<FileItemCard style={style}>
<CardContent>
<FileIcon>{getFileIcon(ext)}</FileIcon>
<Flex vertical gap={0} flex={1} style={{ width: '0px' }}>
<FileIcon>{icon || getFileIcon(ext)}</FileIcon>
<Flex vertical justify="center" gap={0} flex={1} style={{ width: '0px' }}>
<FileName>{name}</FileName>
{extra && <FileInfo>{extra}</FileInfo>}
</Flex>
{actions}
<FileActions>{actions}</FileActions>
</CardContent>
</FileItemCard>
)
@ -96,7 +98,9 @@ const FileItemCard = styled.div`
overflow: hidden;
border: 0.5px solid var(--color-border);
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);
&:hover {
box-shadow:
@ -109,15 +113,19 @@ const FileItemCard = styled.div`
`
const CardContent = styled.div`
padding: 8px 16px;
padding: 8px 8px 8px 16px;
display: flex;
align-items: center;
align-items: stretch;
gap: 16px;
`
const FileIcon = styled.div`
max-height: 44px;
color: var(--color-text-3);
font-size: 32px;
display: flex;
align-items: center;
justify-content: center;
`
const FileName = styled.div`
@ -140,4 +148,11 @@ const FileInfo = styled.div`
color: var(--color-text-2);
`
const FileActions = styled.div`
max-height: 44px;
display: flex;
align-items: center;
justify-content: center;
`
export default memo(FileItem)

View File

@ -66,7 +66,7 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
<VirtualList
data={list}
height={window.innerHeight - 100}
itemHeight={80}
itemHeight={75}
itemKey="key"
style={{ padding: '0 16px 16px 16px' }}
styles={{
@ -80,7 +80,7 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
{(item) => (
<div
style={{
height: '80px',
height: '75px',
paddingTop: '12px'
}}>
<FileItem

View File

@ -1,5 +1,5 @@
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 { QuickPanelListItem } from '@renderer/components/QuickPanel/types'
import { getModelLogo, isEmbeddingModel, isRerankModel } from '@renderer/config/models'
@ -51,7 +51,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
.reverse()
.map((item) => ({
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: (
<Avatar src={getModelLogo(item.model.id)} size={20}>
{first(item.model.name)}

View File

@ -493,7 +493,6 @@ const TopicListItem = styled.div`
}
.menu {
opacity: 1;
background-color: var(--color-background-soft);
&:hover {
color: var(--color-text-2);
}

View File

@ -7,6 +7,7 @@ import {
SearchOutlined,
SettingOutlined
} from '@ant-design/icons'
import CustomTag from '@renderer/components/CustomTag'
import Ellipsis from '@renderer/components/Ellipsis'
import { HStack } from '@renderer/components/Layout'
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 { formatFileSize } from '@renderer/utils'
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 VirtualList from 'rc-virtual-list'
import { FC } from 'react'
@ -269,8 +270,8 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
) : (
<VirtualList
data={fileItems.reverse()}
height={fileItems.length > 5 ? 400 : fileItems.length * 80}
itemHeight={80}
height={fileItems.length > 5 ? 400 : fileItems.length * 75}
itemHeight={75}
itemKey="id"
styles={{
verticalScrollBar: {
@ -283,7 +284,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
{(item) => {
const file = item.content as FileType
return (
<div style={{ height: '80px', paddingTop: '12px' }}>
<div style={{ height: '75px', paddingTop: '12px' }}>
<FileItem
key={item.id}
fileInfo={{
@ -537,20 +538,39 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
))}
</FlexColumn>
</CustomCollapse>
<ModelInfo>
<div className="model-header">
<label>{t('knowledge.model_info')}</label>
<Button icon={<SettingOutlined />} onClick={() => KnowledgeSettingsPopup.show({ base })} size="small" />
</div>
<CustomCollapse
collapsible="icon"
label={
<Flex gap={8} align="center">
{t('knowledge.model_info')}
<Button
type="text"
icon={<SettingOutlined />}
onClick={() => KnowledgeSettingsPopup.show({ base })}
size="small"
/>
</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="label-column">
<label>{t('models.embedding_model')}</label>
</div>
<div className="tag-column">
{providerName && <Tag color="purple">{providerName}</Tag>}
<Tag color="blue">{base.model.name}</Tag>
<Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag>
{providerName && <CustomTag color="#af21af">{providerName}</CustomTag>}
<CustomTag color="#0000ff">{base.model.name}</CustomTag>
<CustomTag color="#00b1b1">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</CustomTag>
</div>
</div>
@ -560,22 +580,13 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<label>{t('models.rerank_model')}</label>
</div>
<div className="tag-column">
{rerankModelProviderName && <Tag color="purple">{rerankModelProviderName}</Tag>}
<Tag color="blue">{base.rerankModel?.name}</Tag>
{rerankModelProviderName && <CustomTag color="#af21af">{rerankModelProviderName}</CustomTag>}
<CustomTag color="#0000ff">{base.rerankModel?.name}</CustomTag>
</div>
</div>
)}
</ModelInfo>
<IndexSection>
<Button
type="primary"
onClick={() => KnowledgeSearchPopup.show({ base })}
icon={<SearchOutlined />}
disabled={disabled}>
{t('knowledge.search')}
</Button>
</IndexSection>
</CustomCollapse>
<BottomSpacer />
</MainContent>
@ -588,9 +599,9 @@ const CollapseLabel = ({ label, count }: { label: string; count: number }) => {
return (
<HStack alignItems="center" gap={10}>
<label>{label}</label>
<Tag style={{ borderRadius: 100, padding: '0 10px' }} color={count ? 'green' : 'default'}>
<CustomTag size={12} color={count ? '#008001' : '#cccccc'}>
{count}
</Tag>
</CustomTag>
</HStack>
)
}
@ -605,12 +616,6 @@ const MainContent = styled(Scrollbar)`
gap: 16px;
`
const IndexSection = styled.div`
margin-top: 20px;
display: flex;
justify-content: center;
`
const ModelInfo = styled.div`
display: flex;
flex-direction: column;
@ -629,6 +634,7 @@ const ModelInfo = styled.div`
display: flex;
align-items: flex-start;
gap: 10px;
margin-top: 16px;
}
.label-column {

View File

@ -120,13 +120,11 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
tooltip={t('settings.models.add.group_name.tooltip')}>
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
<Flex justify="center" align="center" style={{ position: 'relative' }}>
<div>
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Flex justify="end" align="center" style={{ position: 'relative' }}>
<Button type="primary" htmlType="submit" size="middle">
{t('settings.models.add.add_model')}
</Button>
</div>
</Flex>
</Form.Item>
</Form>

View File

@ -1,6 +1,7 @@
import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
import { Center } from '@renderer/components/Layout'
import ModelTags from '@renderer/components/ModelTags'
import { LoadingOutlined, MinusOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'
import CustomCollapse from '@renderer/components/CustomCollapse'
import CustomTag from '@renderer/components/CustomTag'
import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel'
import {
getModelLogo,
isEmbeddingModel,
@ -12,11 +13,12 @@ import {
SYSTEM_MODELS
} from '@renderer/config/models'
import { useProvider } from '@renderer/hooks/useProvider'
import FileItem from '@renderer/pages/files/FileItem'
import { fetchModels } from '@renderer/services/ApiService'
import { Model, Provider } from '@renderer/types'
import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils'
import { Avatar, Button, Empty, Flex, Modal, Popover, Radio, Tooltip } from 'antd'
import Search from 'antd/es/input/Search'
import { Avatar, Button, Empty, Flex, Modal, Tabs, Tooltip, Typography } from 'antd'
import Input from 'antd/es/input/Input'
import { groupBy, isEmpty, uniqBy } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -163,51 +165,63 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
width="680px"
styles={{
content: { padding: 0 },
header: { padding: 22, paddingBottom: 15 }
header: { padding: '16px 22px 30px 22px' }
}}
centered>
<SearchContainer>
<Center>
<Radio.Group
size={i18n.language.startsWith('zh') ? 'middle' : 'small'}
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
<Input
prefix={<SearchOutlined />}
size="large"
ref={searchInputRef}
placeholder={t('settings.provider.search_placeholder')}
allowClear
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>
<ListContainer>
{Object.keys(modelGroups).map((group) => {
{Object.keys(modelGroups).map((group, i) => {
const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id))
return (
<div key={group}>
<ListHeader key={group}>
{group}
<div>
<Button
type="text"
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
<CustomCollapse
key={i}
defaultActiveKey={i >= 5 ? [] : ['1']}
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`)
}
onClick={() => {
placement="top">
<Button
type="text"
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
onClick={(e) => {
e.stopPropagation()
if (isAllInProvider) {
modelGroups[group]
.filter((model) => isModelInProvider(provider, model.id))
@ -217,40 +231,68 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
}
}}
/>
</div>
</ListHeader>
{modelGroups[group].map((model) => {
return (
<ListItem key={model.id}>
<ListItemHeader>
<Avatar src={getModelLogo(model.id)} size={24}>
{model?.name?.[0]?.toUpperCase()}
</Avatar>
</Tooltip>
}>
<FlexColumn>
{modelGroups[group].map((model) => (
<FileItem
style={{
backgroundColor: isModelInProvider(provider, model.id)
? 'rgba(0, 126, 0, 0.06)'
: 'rgba(255, 255, 255, 0.04)'
}}
key={model.id}
fileInfo={{
icon: <Avatar src={getModelLogo(model.id)}>{model?.name?.[0]?.toUpperCase()}</Avatar>,
name: (
<ListItemName>
<Tooltip title={model.id} placement="top">
<Tooltip
styles={{
root: {
width: 'auto',
maxWidth: '500px'
}
}}
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>
<ModelTags model={model} />
{!isEmpty(model.description) && (
<Popover
trigger="click"
title={model.name}
content={model.description}
overlayStyle={{ maxWidth: 600 }}>
<Question />
</Popover>
)}
</ListItemName>
</ListItemHeader>
{isModelInProvider(provider, model.id) ? (
<Button type="default" onClick={() => onRemoveModel(model)} icon={<MinusOutlined />} />
) : (
<Button type="primary" onClick={() => onAddModel(model)} icon={<PlusOutlined />} />
),
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>
)}
</ListItem>
)
})}
</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')} />}
@ -264,7 +306,6 @@ const SearchContainer = styled.div`
flex-direction: column;
gap: 15px;
padding: 0 22px;
padding-bottom: 10px;
margin-top: -10px;
.ant-radio-group {
@ -274,37 +315,21 @@ const SearchContainer = styled.div`
`
const ListContainer = styled.div`
max-height: 70vh;
height: calc(100vh - 300px);
overflow-y: scroll;
padding-bottom: 20px;
`
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;
padding: 0 6px 16px 6px;
margin-left: 16px;
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`
@ -314,8 +339,8 @@ const ListItemName = styled.div`
gap: 10px;
color: var(--color-text);
font-size: 14px;
line-height: 1;
font-weight: 600;
margin-left: 6px;
`
const ModelHeaderTitle = styled.div`
@ -325,11 +350,6 @@ const ModelHeaderTitle = styled.div`
margin-right: 10px;
`
const Question = styled(QuestionCircleOutlined)`
cursor: pointer;
color: #888;
`
export default class EditModelsPopup {
static topviewId = 0
static hide() {

View File

@ -160,7 +160,7 @@ const PopupContainer: React.FC<Props> = ({ title, apiKeys, resolve }) => {
/>
</Space>
</Space>
<Button key="start" type="primary" onClick={onStart}>
<Button key="start" type="primary" onClick={onStart} size="small">
{t('settings.models.check.start')}
</Button>
</Space>

View File

@ -102,18 +102,14 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
<Flex justify="center" align="center" style={{ position: 'relative' }}>
<div>
<Button type="primary" htmlType="submit" size="middle">
{t('common.save')}
</Button>
</div>
<MoreSettingsRow
onClick={() => setShowModelTypes(!showModelTypes)}
style={{ position: 'absolute', right: 0 }}>
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
<MoreSettingsRow onClick={() => setShowModelTypes(!showModelTypes)}>
{t('settings.moresetting')}
<ExpandIcon>{showModelTypes ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
</MoreSettingsRow>
<Button type="primary" htmlType="submit" size="middle">
{t('common.save')}
</Button>
</Flex>
</Form.Item>
{showModelTypes && (

View File

@ -5,20 +5,24 @@ import {
ExclamationCircleFilled,
LoadingOutlined,
MinusCircleOutlined,
MinusOutlined,
PlusOutlined,
SettingOutlined
} 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 { PROVIDER_CONFIG } from '@renderer/config/providers'
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useProvider } from '@renderer/hooks/useProvider'
import FileItem from '@renderer/pages/files/FileItem'
import { ModelCheckStatus } from '@renderer/services/HealthCheckService'
import { useAppDispatch } from '@renderer/store'
import { setModel } from '@renderer/store/assistants'
import { Model } from '@renderer/types'
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 React, { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -240,11 +244,19 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
return (
<>
{Object.keys(sortedModelGroups).map((group) => (
<Card
<Flex gap={12} vertical>
{Object.keys(sortedModelGroups).map((group, i) => (
<CustomCollapse
defaultActiveKey={i <= 5 ? ['1'] : []}
key={group}
type="inner"
title={group}
label={
<Flex align="center" gap={10}>
<span>{group}</span>
<CustomTag color="#02B96B" size={10}>
{modelGroups[group].length}
</CustomTag>
</Flex>
}
extra={
<Tooltip title={t('settings.models.manage.remove_whole_group')}>
<HoveredRemoveIcon
@ -255,41 +267,68 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
}
/>
</Tooltip>
}
style={{ marginBottom: '10px', border: '0.5px solid var(--color-border)' }}
size="small">
}>
<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
console.log('model', model.id, getModelLogo(model.id))
return (
<ModelListItem key={model.id}>
<ModelListHeader>
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
{model?.name?.[0]?.toUpperCase()}
</Avatar>
<ModelNameRow>
<span>{model?.name}</span>
<ModelTags model={model} />
</ModelNameRow>
<SettingIcon
onClick={() => !isChecking && onEditModel(model)}
style={{ cursor: isChecking ? 'not-allowed' : 'pointer', opacity: isChecking ? 0.5 : 1 }}
/>
<FileItem
key={model.id}
fileInfo={{
icon: <Avatar src={getModelLogo(model.id)}>{model?.name?.[0]?.toUpperCase()}</Avatar>,
name: (
<ListItemName>
<Tooltip
styles={{
root: {
width: 'auto',
maxWidth: '500px'
}
}}
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)}
</ModelListHeader>
<Space>
{renderStatusIndicator(modelStatus)}
<RemoveIcon
onClick={() => !isChecking && removeModel(model)}
style={{ cursor: isChecking ? 'not-allowed' : 'pointer', opacity: isChecking ? 0.5 : 1 }}
<Button
type="text"
onClick={() => !isChecking && onEditModel(model)}
disabled={isChecking}
icon={<SettingOutlined />}
/>
<Button
type="text"
onClick={() => !isChecking && removeModel(model)}
disabled={isChecking}
icon={<MinusOutlined />}
/>
</Flex>
)
}}
/>
</Space>
</ModelListItem>
)
})}
</Card>
</Flex>
</CustomCollapse>
))}
{docsWebsite && (
<SettingHelpTextRow>
@ -305,6 +344,7 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
</SettingHelpTextRow>
)}
</Flex>
<Flex gap={10} style={{ marginTop: '10px' }}>
<Button type="primary" onClick={onManageModel} icon={<EditOutlined />}>
{t('button.manage')}
@ -326,25 +366,20 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
)
}
const ModelListItem = 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`
const ListItemName = styled.div`
display: flex;
flex-direction: row;
align-items: center;
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)`
@ -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 }>`
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-size: 14px;
cursor: pointer;
color: ${(props) => {
switch (props.type) {
@ -398,6 +423,7 @@ const StatusIndicator = styled.div<{ type: string }>`
const ModelLatencyText = styled(Typography.Text)`
margin-left: 10px;
color: var(--color-text-secondary);
font-size: 12px;
`
export default memo(ModelList)

View File

@ -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 OAuthButton from '@renderer/components/OAuth/OAuthButton'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
@ -383,7 +384,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
<SettingSubtitle style={{ marginBottom: 5 }}>
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
<Space>
<span>{t('common.models')}</span>
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.models')}</SettingSubtitle>
{!isEmpty(models) && <ModelListSearchBar onSearch={setModelSearchText} />}
</Space>
{!isEmpty(models) && (
@ -391,7 +392,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
<Button
type="text"
size="small"
icon={<HeartOutlined />}
icon={<StreamlineGoodHealthAndWellBeing />}
onClick={onHealthCheck}
loading={isHealthChecking}
/>

View File

@ -12,7 +12,7 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
padding-top: 15px;
overflow-y: scroll;
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 {
display: none;