mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 11:49:02 +08:00
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:
parent
cd1b0e01a0
commit
76c025d53b
113
src/renderer/src/components/ProviderLogoPicker/index.tsx
Normal file
113
src/renderer/src/components/ProviderLogoPicker/index.tsx
Normal 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
|
||||||
@ -594,7 +594,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
|||||||
|
|
||||||
export const SYSTEM_PROVIDERS: SystemProvider[] = Object.values(SYSTEM_PROVIDERS_CONFIG)
|
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,
|
ph8: Ph8ProviderLogo,
|
||||||
'302ai': Ai302ProviderLogo,
|
'302ai': Ai302ProviderLogo,
|
||||||
openai: OpenAiProviderLogo,
|
openai: OpenAiProviderLogo,
|
||||||
|
|||||||
@ -2679,7 +2679,8 @@
|
|||||||
"title": "Auto Update"
|
"title": "Auto Update"
|
||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
"reset": "Reset Avatar"
|
"builtin": "Builtin avatar",
|
||||||
|
"reset": "Reset avatar"
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
"button": "Backup",
|
"button": "Backup",
|
||||||
|
|||||||
@ -2679,6 +2679,7 @@
|
|||||||
"title": "自動更新"
|
"title": "自動更新"
|
||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
|
"builtin": "内蔵アバター",
|
||||||
"reset": "アバターをリセット"
|
"reset": "アバターをリセット"
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
|
|||||||
@ -2679,6 +2679,7 @@
|
|||||||
"title": "Автоматическое обновление"
|
"title": "Автоматическое обновление"
|
||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
|
"builtin": "Встроенный аватар",
|
||||||
"reset": "Сброс аватара"
|
"reset": "Сброс аватара"
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
|
|||||||
@ -2679,6 +2679,7 @@
|
|||||||
"title": "自动更新"
|
"title": "自动更新"
|
||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
|
"builtin": "内置头像",
|
||||||
"reset": "重置头像"
|
"reset": "重置头像"
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
|
|||||||
@ -2679,6 +2679,7 @@
|
|||||||
"title": "自動更新"
|
"title": "自動更新"
|
||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
|
"builtin": "內置頭像",
|
||||||
"reset": "重設頭像"
|
"reset": "重設頭像"
|
||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { Center, VStack } from '@renderer/components/Layout'
|
import { Center, VStack } from '@renderer/components/Layout'
|
||||||
|
import ProviderLogoPicker from '@renderer/components/ProviderLogoPicker'
|
||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
|
import { PROVIDER_LOGO_MAP } from '@renderer/config/providers'
|
||||||
import ImageStorage from '@renderer/services/ImageStorage'
|
import ImageStorage from '@renderer/services/ImageStorage'
|
||||||
import { Provider, ProviderType } from '@renderer/types'
|
import { Provider, ProviderType } from '@renderer/types'
|
||||||
import { compressImage, generateColorFromChar } from '@renderer/utils'
|
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 React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -21,6 +23,7 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
|||||||
const [name, setName] = useState(provider?.name || '')
|
const [name, setName] = useState(provider?.name || '')
|
||||||
const [type, setType] = useState<ProviderType>(provider?.type || 'openai')
|
const [type, setType] = useState<ProviderType>(provider?.type || 'openai')
|
||||||
const [logo, setLogo] = useState<string | null>(null)
|
const [logo, setLogo] = useState<string | null>(null)
|
||||||
|
const [logoPickerOpen, setLogoPickerOpen] = useState(false)
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -63,6 +66,25 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
|||||||
|
|
||||||
const buttonDisabled = name.length === 0
|
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 () => {
|
const handleReset = async () => {
|
||||||
try {
|
try {
|
||||||
setLogo(null)
|
setLogo(null)
|
||||||
@ -131,6 +153,20 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
|||||||
</div>
|
</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',
|
key: 'reset',
|
||||||
label: (
|
label: (
|
||||||
@ -170,15 +206,30 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
|||||||
placement="bottom"
|
placement="bottom"
|
||||||
onOpenChange={(visible) => {
|
onOpenChange={(visible) => {
|
||||||
setDropdownOpen(visible)
|
setDropdownOpen(visible)
|
||||||
|
if (visible) {
|
||||||
|
setLogoPickerOpen(false)
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
{logo ? (
|
<Popover
|
||||||
<ProviderLogo src={logo} />
|
content={<ProviderLogoPicker onProviderClick={handleProviderLogoClick} />}
|
||||||
) : (
|
trigger="click"
|
||||||
<ProviderInitialsLogo
|
open={logoPickerOpen}
|
||||||
style={name ? { backgroundColor: generateColorFromChar(name) } : { color: 'black' }}>
|
onOpenChange={(visible) => {
|
||||||
{getInitials()}
|
setLogoPickerOpen(visible)
|
||||||
</ProviderInitialsLogo>
|
if (visible) {
|
||||||
)}
|
setDropdownOpen(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placement="bottom">
|
||||||
|
{logo ? (
|
||||||
|
<ProviderLogo src={logo} />
|
||||||
|
) : (
|
||||||
|
<ProviderInitialsLogo
|
||||||
|
style={name ? { backgroundColor: generateColorFromChar(name) } : { color: 'black' }}>
|
||||||
|
{getInitials()}
|
||||||
|
</ProviderInitialsLogo>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Center>
|
</Center>
|
||||||
|
|||||||
@ -327,7 +327,7 @@ const ProvidersList: FC = () => {
|
|||||||
if (name) {
|
if (name) {
|
||||||
updateProvider({ ...provider, name, type })
|
updateProvider({ ...provider, name, type })
|
||||||
if (provider.id) {
|
if (provider.id) {
|
||||||
if (logoFile && logo) {
|
if (logo) {
|
||||||
try {
|
try {
|
||||||
await ImageStorage.set(`provider-${provider.id}`, logo)
|
await ImageStorage.set(`provider-${provider.id}`, logo)
|
||||||
setProviderLogos((prev) => ({
|
setProviderLogos((prev) => ({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user