Feat/add built-in provider avatar options when adding a provider (#9350)

* Add 'builtin avatar' option to avatar dropdown

-Introduces a new 'builtin avatar' option to the avatar selection dropdown in AddProviderPopup.
-Updates i18n translation files for all supported languages to include the 'builtin' avatar label.

Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com>

* Add provider logo picker for builtin avatar selection

-Introduces a ProviderLogoPicker component for selecting a builtin provider logo as an avatar in AddProviderPopup.
-Updates provider logo handling in ProviderSettings.(If deleting the logoFile caused any issues, I sincerely apologize.)

Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com>

* Adjust ProviderLogoPicker layout dimensions and grid

Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com>

* Fix ProviderLogoPicker popover trigger behavior

Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com>

* Merge branch 'main' into feat/add-builtin-provider-avatars

* Update index.tsx

---------

Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com>
This commit is contained in:
Yuhang 2025-08-22 09:42:24 +08:00 committed by GitHub
parent cd1b0e01a0
commit 76c025d53b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 181 additions and 12 deletions

View File

@ -0,0 +1,113 @@
import { SearchOutlined } from '@ant-design/icons'
import { PROVIDER_LOGO_MAP } from '@renderer/config/providers'
import { getProviderLabel } from '@renderer/i18n/label'
import { Input, Tooltip } from 'antd'
import { FC, useMemo, useState } from 'react'
import styled from 'styled-components'
interface Props {
onProviderClick: (providerId: string) => void
}
// 用于选择内置头像的提供商Logo选择器组件
const ProviderLogoPicker: FC<Props> = ({ onProviderClick }) => {
const [searchText, setSearchText] = useState('')
const filteredProviders = useMemo(() => {
const providers = Object.entries(PROVIDER_LOGO_MAP).map(([id, logo]) => ({
id,
logo,
name: getProviderLabel(id)
}))
if (!searchText) return providers
const searchLower = searchText.toLowerCase()
return providers.filter((p) => p.name.toLowerCase().includes(searchLower))
}, [searchText])
const handleProviderClick = (event: React.MouseEvent, providerId: string) => {
event.stopPropagation()
onProviderClick(providerId)
}
return (
<Container>
<SearchContainer>
<Input
placeholder="search"
prefix={<SearchOutlined style={{ color: 'var(--color-text-3)' }} />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
size="small"
allowClear
style={{
borderRadius: 'var(--list-item-border-radius)',
background: 'var(--color-background-soft)'
}}
/>
</SearchContainer>
<LogoGrid>
{filteredProviders.map(({ id, logo, name }) => (
<Tooltip key={id} title={name} placement="top" mouseLeaveDelay={0}>
<LogoItem onClick={(e) => handleProviderClick(e, id)}>
<img src={logo} alt={name} draggable={false} />
</LogoItem>
</Tooltip>
))}
</LogoGrid>
</Container>
)
}
const Container = styled.div`
width: 350px;
max-height: 300px;
display: flex;
flex-direction: column;
padding: 12px;
background: var(--color-background);
border-radius: 8px;
`
const SearchContainer = styled.div`
margin-bottom: 12px;
`
const LogoGrid = styled.div`
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
overflow-y: auto;
flex: 1;
padding: 4px;
`
const LogoItem = styled.div`
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s ease;
background: var(--color-background-soft);
border: 0.5px solid var(--color-border);
&:hover {
background: var(--color-background-mute);
transform: scale(1.05);
border-color: var(--color-primary);
}
img {
width: 32px;
height: 32px;
object-fit: contain;
user-select: none;
-webkit-user-drag: none;
}
`
export default ProviderLogoPicker

View File

@ -594,7 +594,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
export const SYSTEM_PROVIDERS: SystemProvider[] = Object.values(SYSTEM_PROVIDERS_CONFIG)
const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
ph8: Ph8ProviderLogo,
'302ai': Ai302ProviderLogo,
openai: OpenAiProviderLogo,

View File

@ -2679,7 +2679,8 @@
"title": "Auto Update"
},
"avatar": {
"reset": "Reset Avatar"
"builtin": "Builtin avatar",
"reset": "Reset avatar"
},
"backup": {
"button": "Backup",

View File

@ -2679,6 +2679,7 @@
"title": "自動更新"
},
"avatar": {
"builtin": "内蔵アバター",
"reset": "アバターをリセット"
},
"backup": {

View File

@ -2679,6 +2679,7 @@
"title": "Автоматическое обновление"
},
"avatar": {
"builtin": "Встроенный аватар",
"reset": "Сброс аватара"
},
"backup": {

View File

@ -2679,6 +2679,7 @@
"title": "自动更新"
},
"avatar": {
"builtin": "内置头像",
"reset": "重置头像"
},
"backup": {

View File

@ -2679,6 +2679,7 @@
"title": "自動更新"
},
"avatar": {
"builtin": "內置頭像",
"reset": "重設頭像"
},
"backup": {

View File

@ -1,10 +1,12 @@
import { loggerService } from '@logger'
import { Center, VStack } from '@renderer/components/Layout'
import ProviderLogoPicker from '@renderer/components/ProviderLogoPicker'
import { TopView } from '@renderer/components/TopView'
import { PROVIDER_LOGO_MAP } from '@renderer/config/providers'
import ImageStorage from '@renderer/services/ImageStorage'
import { Provider, ProviderType } from '@renderer/types'
import { compressImage, generateColorFromChar } from '@renderer/utils'
import { Divider, Dropdown, Form, Input, Modal, Select, Upload } from 'antd'
import { Divider, Dropdown, Form, Input, Modal, Popover, Select, Upload } from 'antd'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -21,6 +23,7 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
const [name, setName] = useState(provider?.name || '')
const [type, setType] = useState<ProviderType>(provider?.type || 'openai')
const [logo, setLogo] = useState<string | null>(null)
const [logoPickerOpen, setLogoPickerOpen] = useState(false)
const [dropdownOpen, setDropdownOpen] = useState(false)
const { t } = useTranslation()
@ -63,6 +66,25 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
const buttonDisabled = name.length === 0
// 处理内置头像的点击事件
const handleProviderLogoClick = async (providerId: string) => {
try {
const logoUrl = PROVIDER_LOGO_MAP[providerId]
if (provider?.id) {
await ImageStorage.set(`provider-${provider.id}`, logoUrl)
const savedLogo = await ImageStorage.get(`provider-${provider.id}`)
setLogo(savedLogo)
} else {
setLogo(logoUrl)
}
setLogoPickerOpen(false)
} catch (error: any) {
window.message.error(error.message)
}
}
const handleReset = async () => {
try {
setLogo(null)
@ -131,6 +153,20 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
</div>
)
},
{
key: 'builtin',
label: (
<div
style={{ width: '100%', textAlign: 'center' }}
onClick={(e) => {
e.stopPropagation()
setDropdownOpen(false)
setLogoPickerOpen(true)
}}>
{t('settings.general.avatar.builtin')}
</div>
)
},
{
key: 'reset',
label: (
@ -170,15 +206,30 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
placement="bottom"
onOpenChange={(visible) => {
setDropdownOpen(visible)
if (visible) {
setLogoPickerOpen(false)
}
}}>
{logo ? (
<ProviderLogo src={logo} />
) : (
<ProviderInitialsLogo
style={name ? { backgroundColor: generateColorFromChar(name) } : { color: 'black' }}>
{getInitials()}
</ProviderInitialsLogo>
)}
<Popover
content={<ProviderLogoPicker onProviderClick={handleProviderLogoClick} />}
trigger="click"
open={logoPickerOpen}
onOpenChange={(visible) => {
setLogoPickerOpen(visible)
if (visible) {
setDropdownOpen(false)
}
}}
placement="bottom">
{logo ? (
<ProviderLogo src={logo} />
) : (
<ProviderInitialsLogo
style={name ? { backgroundColor: generateColorFromChar(name) } : { color: 'black' }}>
{getInitials()}
</ProviderInitialsLogo>
)}
</Popover>
</Dropdown>
</VStack>
</Center>

View File

@ -327,7 +327,7 @@ const ProvidersList: FC = () => {
if (name) {
updateProvider({ ...provider, name, type })
if (provider.id) {
if (logoFile && logo) {
if (logo) {
try {
await ImageStorage.set(`provider-${provider.id}`, logo)
setProviderLogos((prev) => ({